From 5af8d8597e93b969218eac86c0fc32d86ffb3726 Mon Sep 17 00:00:00 2001
From: Daniel Gerhardt <code@dgerhardt.net>
Date: Tue, 15 Jan 2019 12:17:01 +0100
Subject: [PATCH] Use event system to handle cascading deletes

Child entities are now deleted by an event handler of their service
class.
---
 .../arsnova/controller/v2/RoomController.java |  2 +-
 .../arsnova/persistence/AnswerRepository.java |  5 +-
 .../persistence/CommentRepository.java        |  4 +-
 .../persistence/ContentRepository.java        |  4 +-
 .../couchdb/CouchDbAnswerRepository.java      | 82 +++----------------
 .../couchdb/CouchDbCommentRepository.java     | 34 ++------
 .../couchdb/CouchDbContentRepository.java     | 31 +++----
 .../couchdb/CouchDbCrudRepository.java        | 28 +++++++
 .../arsnova/service/AnswerServiceImpl.java    | 21 ++++-
 .../arsnova/service/CommentServiceImpl.java   | 14 +++-
 .../arsnova/service/ContentServiceImpl.java   | 73 +++++++----------
 .../de/thm/arsnova/service/RoomService.java   |  2 -
 .../thm/arsnova/service/RoomServiceImpl.java  | 31 +------
 .../thm/arsnova/service/TimerServiceImpl.java |  7 +-
 14 files changed, 128 insertions(+), 210 deletions(-)

diff --git a/src/main/java/de/thm/arsnova/controller/v2/RoomController.java b/src/main/java/de/thm/arsnova/controller/v2/RoomController.java
index 981d13b73..db8b9d6eb 100644
--- a/src/main/java/de/thm/arsnova/controller/v2/RoomController.java
+++ b/src/main/java/de/thm/arsnova/controller/v2/RoomController.java
@@ -97,7 +97,7 @@ public class RoomController extends PaginationController {
 	@RequestMapping(value = "/{shortId}", method = RequestMethod.DELETE)
 	public void deleteRoom(@ApiParam(value = "Room-Key from current Room", required = true) @PathVariable final String shortId) {
 		de.thm.arsnova.model.Room room = roomService.getByShortId(shortId);
-		roomService.deleteCascading(room);
+		roomService.delete(room);
 	}
 
 	@ApiOperation(value = "count active users",
diff --git a/src/main/java/de/thm/arsnova/persistence/AnswerRepository.java b/src/main/java/de/thm/arsnova/persistence/AnswerRepository.java
index 3bf6a1e25..b25622e51 100644
--- a/src/main/java/de/thm/arsnova/persistence/AnswerRepository.java
+++ b/src/main/java/de/thm/arsnova/persistence/AnswerRepository.java
@@ -29,10 +29,9 @@ public interface AnswerRepository extends CrudRepository<Answer, String> {
 	int countByContentId(String contentId);
 	<T extends Answer> List<T> findByContentId(String contentId, Class<T> type, int start, int limit);
 	List<Answer> findByUserIdRoomId(String userId, String roomId);
+	Iterable<Answer> findStubsByContentId(String contentId);
+	Iterable<Answer> findStubsByContentIds(List<String> contentId);
 	int countByRoomId(String roomId);
-	int deleteByContentId(String contentId);
 	int countByRoomIdOnlyLectureVariant(String roomId);
 	int countByRoomIdOnlyPreparationVariant(String roomId);
-	int deleteAllAnswersForQuestions(List<String> contentIds);
-	int deleteByContentIds(List<String> contentIds);
 }
diff --git a/src/main/java/de/thm/arsnova/persistence/CommentRepository.java b/src/main/java/de/thm/arsnova/persistence/CommentRepository.java
index 9624e755a..d8735a96a 100644
--- a/src/main/java/de/thm/arsnova/persistence/CommentRepository.java
+++ b/src/main/java/de/thm/arsnova/persistence/CommentRepository.java
@@ -11,7 +11,7 @@ public interface CommentRepository extends CrudRepository<Comment, String> {
 	CommentReadingCount countReadingByRoomIdAndUserId(String roomId, String userId);
 	List<Comment> findByRoomId(String roomId, int start, int limit);
 	List<Comment> findByRoomIdAndUserId(String roomId, String userId, int start, int limit);
+	Iterable<Comment> findStubsByRoomId(String roomId);
+	Iterable<Comment> findStubsByRoomIdAndUserId(String roomId, String userId);
 	Comment findOne(String commentId);
-	int deleteByRoomId(String roomId);
-	int deleteByRoomIdAndUserId(String roomId, String userId);
 }
diff --git a/src/main/java/de/thm/arsnova/persistence/ContentRepository.java b/src/main/java/de/thm/arsnova/persistence/ContentRepository.java
index eca83a089..098eef74d 100644
--- a/src/main/java/de/thm/arsnova/persistence/ContentRepository.java
+++ b/src/main/java/de/thm/arsnova/persistence/ContentRepository.java
@@ -10,8 +10,8 @@ public interface ContentRepository extends CrudRepository<Content, String> {
 	List<Content> findByRoomIdForSpeaker(String roomId);
 	int countByRoomId(String roomId);
 	List<String> findIdsByRoomId(String roomId);
-	List<String> findIdsByRoomIdAndVariant(String roomId, String variant);
-	int deleteByRoomId(String roomId);
+	Iterable<Content> findStubsByRoomId(final String roomId);
+	Iterable<Content> findStubsByRoomIdAndVariant(String roomId, String variant);
 	List<String> findUnansweredIdsByRoomIdAndUser(String roomId, String userId);
 	List<Content> findByRoomIdOnlyLectureVariantAndActive(String roomId);
 	List<Content> findByRoomIdOnlyLectureVariant(String roomId);
diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbAnswerRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbAnswerRepository.java
index 9b1d3d6e0..58a195682 100644
--- a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbAnswerRepository.java
+++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbAnswerRepository.java
@@ -1,16 +1,12 @@
 package de.thm.arsnova.persistence.couchdb;
 
 import com.fasterxml.jackson.databind.JsonNode;
-import com.google.common.collect.Lists;
 import de.thm.arsnova.model.Answer;
 import de.thm.arsnova.model.AnswerStatistics;
 import de.thm.arsnova.persistence.AnswerRepository;
 import de.thm.arsnova.persistence.LogEntryRepository;
-import org.ektorp.BulkDeleteDocument;
 import org.ektorp.ComplexKey;
 import org.ektorp.CouchDbConnector;
-import org.ektorp.DbAccessException;
-import org.ektorp.DocumentOperationResult;
 import org.ektorp.ViewResult;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -26,7 +22,6 @@ import java.util.List;
 import java.util.Map;
 
 public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> implements AnswerRepository, ApplicationEventPublisherAware {
-	private static final int BULK_PARTITION_SIZE = 500;
 	private static final Logger logger = LoggerFactory.getLogger(CouchDbAnswerRepository.class);
 
 	@Autowired
@@ -43,34 +38,18 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> imple
 		this.publisher = publisher;
 	}
 
-	@Override
-	public int deleteByContentId(final String contentId) {
-		try {
-			final ViewResult result = db.queryView(createQuery("by_contentid")
-					.key(contentId));
-			final List<List<ViewResult.Row>> partitions = Lists.partition(result.getRows(), BULK_PARTITION_SIZE);
-
-			int count = 0;
-			for (final List<ViewResult.Row> partition: partitions) {
-				final List<BulkDeleteDocument> answersToDelete = new ArrayList<>();
-				for (final ViewResult.Row a : partition) {
-					final BulkDeleteDocument d = new BulkDeleteDocument(a.getId(), a.getValueAsNode().get("_rev").asText());
-					answersToDelete.add(d);
-				}
-				final List<DocumentOperationResult> errors = db.executeBulk(answersToDelete);
-				count += partition.size() - errors.size();
-				if (errors.size() > 0) {
-					logger.error("Could not bulk delete {} of {} answers.", errors.size(), partition.size());
-				}
-			}
-			dbLogger.log("delete", "type", "answer", "answerCount", count);
+	protected Iterable<Answer> createEntityStubs(final ViewResult viewResult) {
+		return super.createEntityStubs(viewResult, Answer::setContentId);
+	}
 
-			return count;
-		} catch (final DbAccessException e) {
-			logger.error("Could not delete answers for content {}.", contentId, e);
-		}
+	@Override
+	public Iterable<Answer> findStubsByContentId(final String contentId) {
+		return createEntityStubs(db.queryView(createQuery("by_contentid").key(contentId)));
+	}
 
-		return 0;
+	@Override
+	public Iterable<Answer> findStubsByContentIds(final List<String> contentIds) {
+		return createEntityStubs(db.queryView(createQuery("by_contentid").keys(contentIds)));
 	}
 
 	@Override
@@ -192,45 +171,4 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> imple
 
 		return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt();
 	}
-
-	@Override
-	public int deleteAllAnswersForQuestions(final List<String> contentIds) {
-		final ViewResult result = db.queryView(createQuery("by_contentid")
-				.keys(contentIds));
-		final List<BulkDeleteDocument> allAnswers = new ArrayList<>();
-		for (final ViewResult.Row a : result.getRows()) {
-			final BulkDeleteDocument d = new BulkDeleteDocument(a.getId(), a.getValueAsNode().get("_rev").asText());
-			allAnswers.add(d);
-		}
-		try {
-			final List<DocumentOperationResult> errors = db.executeBulk(allAnswers);
-
-			return allAnswers.size() - errors.size();
-		} catch (final DbAccessException e) {
-			logger.error("Could not bulk delete answers.", e);
-		}
-
-		return 0;
-	}
-
-	@Override
-	public int deleteByContentIds(final List<String> contentIds) {
-		final ViewResult result = db.queryView(createQuery("by_contentid")
-				.keys(contentIds));
-		final List<BulkDeleteDocument> deleteDocs = new ArrayList<>();
-		for (final ViewResult.Row a : result.getRows()) {
-			final BulkDeleteDocument d = new BulkDeleteDocument(a.getId(), a.getValueAsNode().get("_rev").asText());
-			deleteDocs.add(d);
-		}
-
-		try {
-			final List<DocumentOperationResult> errors = db.executeBulk(deleteDocs);
-
-			return deleteDocs.size() - errors.size();
-		} catch (final DbAccessException e) {
-			logger.error("Could not bulk delete answers.", e);
-		}
-
-		return 0;
-	}
 }
diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbCommentRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbCommentRepository.java
index 32e28fb08..8a355c416 100644
--- a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbCommentRepository.java
+++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbCommentRepository.java
@@ -7,7 +7,6 @@ import de.thm.arsnova.persistence.CommentRepository;
 import de.thm.arsnova.persistence.LogEntryRepository;
 import org.ektorp.ComplexKey;
 import org.ektorp.CouchDbConnector;
-import org.ektorp.UpdateConflictException;
 import org.ektorp.ViewResult;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -142,37 +141,18 @@ public class CouchDbCommentRepository extends CouchDbCrudRepository<Comment> imp
 	}
 
 	@Override
-	public int deleteByRoomId(final String roomId) {
-		final ViewResult result = db.queryView(createQuery("by_roomid").key(roomId));
-
-		return delete(result);
+	public Iterable<Comment> findStubsByRoomId(final String roomId) {
+		return createEntityStubs(db.queryView(createQuery("by_roomid").key(roomId).reduce(false)));
 	}
 
 	@Override
-	public int deleteByRoomIdAndUserId(final String roomId, final String userId) {
-		final ViewResult result = db.queryView(createQuery("by_roomid_creatorid_read")
+	public Iterable<Comment> findStubsByRoomIdAndUserId(final String roomId, final String userId) {
+		return createEntityStubs(db.queryView(createQuery("by_roomid_creatorid_read")
 				.startKey(ComplexKey.of(roomId, userId))
-				.endKey(ComplexKey.of(roomId, userId, ComplexKey.emptyObject())));
-
-		return delete(result);
+				.endKey(ComplexKey.of(roomId, userId, ComplexKey.emptyObject()))));
 	}
 
-	private int delete(final ViewResult comments) {
-		if (comments.isEmpty()) {
-			return 0;
-		}
-		/* TODO: use bulk delete */
-		for (final ViewResult.Row row : comments.getRows()) {
-			try {
-				db.delete(row.getId(), row.getValueAsNode().get("rev").asText());
-			} catch (final UpdateConflictException e) {
-				logger.error("Could not delete comments.", e);
-			}
-		}
-
-		/* This does account for failed deletions */
-		dbLogger.log("delete", "type", "comment", "commentCount", comments.getSize());
-
-		return comments.getSize();
+	protected Iterable<Comment> createEntityStubs(final ViewResult viewResult) {
+		return super.createEntityStubs(viewResult, Comment::setRoomId);
 	}
 }
diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentRepository.java
index 73aa0550e..a475cb3c7 100644
--- a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentRepository.java
+++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentRepository.java
@@ -3,10 +3,8 @@ package de.thm.arsnova.persistence.couchdb;
 import de.thm.arsnova.model.Content;
 import de.thm.arsnova.persistence.ContentRepository;
 import de.thm.arsnova.persistence.LogEntryRepository;
-import org.ektorp.BulkDeleteDocument;
 import org.ektorp.ComplexKey;
 import org.ektorp.CouchDbConnector;
-import org.ektorp.DocumentOperationResult;
 import org.ektorp.ViewResult;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -67,28 +65,23 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp
 	}
 
 	@Override
-	public List<String> findIdsByRoomIdAndVariant(final String roomId, final String variant) {
-		return collectQuestionIds(db.queryView(createQuery("by_roomid_group_locked")
-				.startKey(ComplexKey.of(roomId, variant))
-				.endKey(ComplexKey.of(roomId, variant, ComplexKey.emptyObject()))
+	public Iterable<Content> findStubsByRoomId(final String roomId) {
+		return createEntityStubs(db.queryView(createQuery("by_roomid_group_locked")
+				.startKey(ComplexKey.of(roomId))
+				.endKey(ComplexKey.of(roomId, ComplexKey.emptyObject()))
 				.reduce(false)));
 	}
 
 	@Override
-	public int deleteByRoomId(final String roomId) {
-		final ViewResult result = db.queryView(createQuery("by_roomid_group_locked")
-				.startKey(ComplexKey.of(roomId))
-				.endKey(ComplexKey.of(roomId, ComplexKey.emptyObject()))
-				.reduce(false));
-
-		final List<BulkDeleteDocument> deleteDocs = new ArrayList<>();
-		for (final ViewResult.Row a : result.getRows()) {
-			final BulkDeleteDocument d = new BulkDeleteDocument(a.getId(), a.getValueAsNode().get("_rev").asText());
-			deleteDocs.add(d);
-		}
-		List<DocumentOperationResult> errors = db.executeBulk(deleteDocs);
+	public Iterable<Content> findStubsByRoomIdAndVariant(final String roomId, final String variant) {
+		return createEntityStubs(db.queryView(createQuery("by_roomid_group_locked")
+				.startKey(ComplexKey.of(roomId, variant))
+				.endKey(ComplexKey.of(roomId, variant, ComplexKey.emptyObject()))
+				.reduce(false)));
+	}
 
-		return deleteDocs.size() - errors.size();
+	protected Iterable<Content> createEntityStubs(final ViewResult viewResult) {
+		return super.createEntityStubs(viewResult, Content::setRoomId);
 	}
 
 	@Override
diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbCrudRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbCrudRepository.java
index 39285cd0d..06da52e30 100644
--- a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbCrudRepository.java
+++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbCrudRepository.java
@@ -4,6 +4,7 @@ import de.thm.arsnova.model.Entity;
 import de.thm.arsnova.persistence.CrudRepository;
 import org.ektorp.BulkDeleteDocument;
 import org.ektorp.CouchDbConnector;
+import org.ektorp.ViewResult;
 import org.ektorp.support.CouchDbRepositorySupport;
 import org.springframework.data.repository.NoRepositoryBean;
 
@@ -11,6 +12,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
+import java.util.function.BiConsumer;
 import java.util.stream.Collectors;
 
 @NoRepositoryBean
@@ -129,4 +131,30 @@ abstract class CouchDbCrudRepository<T extends Entity> extends CouchDbRepository
 	public void deleteAll() {
 		throw new UnsupportedOperationException("Deletion of all entities is not supported for security reasons.");
 	}
+
+	/**
+	 * Creates stub entities from a ViewResult. Stub entities only have meta data (id, revision, reference id) set.
+	 *
+	 * @param viewResult A CouchDB ViewResult. The first part of its keys is expected to be the id of another entity.
+	 * @param keyPropertySetter A setter method of the Entity class which is called to store the first element of the
+	 *   key.
+	 * @return Entity stubs
+	 */
+	protected Iterable<T> createEntityStubs(final ViewResult viewResult, final BiConsumer<T, String> keyPropertySetter) {
+		return viewResult.getRows().stream().map(row -> {
+			final T stub;
+			try {
+				stub = type.newInstance();
+				stub.setId(row.getId());
+				stub.setRevision(row.getValueAsNode().get("_rev").asText());
+				final String key = row.getKeyAsNode().isContainerNode()
+						? row.getKeyAsNode().get(0).asText() : row.getKey();
+				keyPropertySetter.accept(stub, key);
+
+				return stub;
+			} catch (InstantiationException | IllegalAccessException e) {
+				return null;
+			}
+		}).collect(Collectors.toList());
+	}
 }
diff --git a/src/main/java/de/thm/arsnova/service/AnswerServiceImpl.java b/src/main/java/de/thm/arsnova/service/AnswerServiceImpl.java
index 8bf36e4fc..070bebb45 100644
--- a/src/main/java/de/thm/arsnova/service/AnswerServiceImpl.java
+++ b/src/main/java/de/thm/arsnova/service/AnswerServiceImpl.java
@@ -19,6 +19,7 @@ package de.thm.arsnova.service;
 
 import de.thm.arsnova.event.AfterCreationEvent;
 import de.thm.arsnova.event.BeforeCreationEvent;
+import de.thm.arsnova.event.BeforeDeletionEvent;
 import de.thm.arsnova.model.Answer;
 import de.thm.arsnova.model.AnswerStatistics;
 import de.thm.arsnova.model.ChoiceQuestionContent;
@@ -33,10 +34,13 @@ import de.thm.arsnova.web.exceptions.UnauthorizedException;
 import org.ektorp.DbAccessException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.context.event.EventListener;
 import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
 import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.security.access.annotation.Secured;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Service;
 
@@ -64,17 +68,19 @@ public class AnswerServiceImpl extends DefaultEntityServiceImpl<Answer> implemen
 	public AnswerServiceImpl(
 			AnswerRepository repository,
 			RoomService roomService,
-			ContentService contentService,
 			UserService userService,
 			@Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) {
 		super(Answer.class, repository, jackson2HttpMessageConverter.getObjectMapper());
 		this.answerRepository = repository;
 		this.roomService = roomService;
-		this.contentService = contentService;
-		this.contentService = contentService;
 		this.userService = userService;
 	}
 
+	@Autowired
+	public void setContentService(final ContentService contentService) {
+		this.contentService = contentService;
+	}
+
 	@Scheduled(fixedDelay = 5000)
 	public void flushAnswerQueue() {
 		if (answerQueue.isEmpty()) {
@@ -110,7 +116,7 @@ public class AnswerServiceImpl extends DefaultEntityServiceImpl<Answer> implemen
 		content.resetState();
 		/* FIXME: cancel timer */
 		contentService.update(content);
-		answerRepository.deleteByContentId(content.getId());
+		delete(answerRepository.findStubsByContentId(content.getId()));
 	}
 
 	@Override
@@ -421,4 +427,11 @@ public class AnswerServiceImpl extends DefaultEntityServiceImpl<Answer> implemen
 	public int countPreparationQuestionAnswersInternal(final String roomId) {
 		return answerRepository.countByRoomIdOnlyPreparationVariant(roomService.get(roomId).getId());
 	}
+
+	@EventListener
+	@Secured({"ROLE_USER", "RUN_AS_SYSTEM"})
+	public void handleContentDeletion(final BeforeDeletionEvent<Content> event) {
+		final Iterable<Answer> answers = answerRepository.findStubsByContentId(event.getEntity().getId());
+		delete(answers);
+	}
 }
diff --git a/src/main/java/de/thm/arsnova/service/CommentServiceImpl.java b/src/main/java/de/thm/arsnova/service/CommentServiceImpl.java
index 69c1241f4..0216263e1 100644
--- a/src/main/java/de/thm/arsnova/service/CommentServiceImpl.java
+++ b/src/main/java/de/thm/arsnova/service/CommentServiceImpl.java
@@ -1,5 +1,6 @@
 package de.thm.arsnova.service;
 
+import de.thm.arsnova.event.BeforeDeletionEvent;
 import de.thm.arsnova.model.Comment;
 import de.thm.arsnova.model.Room;
 import de.thm.arsnova.model.migration.v2.CommentReadingCount;
@@ -9,7 +10,9 @@ import de.thm.arsnova.web.exceptions.ForbiddenException;
 import de.thm.arsnova.web.exceptions.NotFoundException;
 import de.thm.arsnova.web.exceptions.UnauthorizedException;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.event.EventListener;
 import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.security.access.annotation.Secured;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Service;
 
@@ -63,9 +66,9 @@ public class CommentServiceImpl extends DefaultEntityServiceImpl<Comment> implem
 		}
 		final User user = getCurrentUser();
 		if (room.getOwnerId().equals(user.getId())) {
-			commentRepository.deleteByRoomId(room.getId());
+			delete(commentRepository.findStubsByRoomId(room.getId()));
 		} else {
-			commentRepository.deleteByRoomIdAndUserId(room.getId(), user.getId());
+			delete(commentRepository.findStubsByRoomIdAndUserId(room.getId(), user.getId()));
 		}
 	}
 
@@ -123,4 +126,11 @@ public class CommentServiceImpl extends DefaultEntityServiceImpl<Comment> implem
 		}
 		return user;
 	}
+
+	@EventListener
+	@Secured({"ROLE_USER", "RUN_AS_SYSTEM"})
+	public void handleRoomDeletion(final BeforeDeletionEvent<Room> event) {
+		final Iterable<Comment> comments = commentRepository.findStubsByRoomId(event.getEntity().getId());
+		delete(comments);
+	}
 }
diff --git a/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java b/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java
index 6bf706dc9..4f8cf552a 100644
--- a/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java
+++ b/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java
@@ -17,6 +17,7 @@
  */
 package de.thm.arsnova.service;
 
+import de.thm.arsnova.event.BeforeDeletionEvent;
 import de.thm.arsnova.model.Content;
 import de.thm.arsnova.model.Room;
 import de.thm.arsnova.model.Room.ContentGroup;
@@ -26,14 +27,15 @@ import de.thm.arsnova.persistence.LogEntryRepository;
 import de.thm.arsnova.security.User;
 import de.thm.arsnova.web.exceptions.NotFoundException;
 import de.thm.arsnova.web.exceptions.UnauthorizedException;
-import org.ektorp.DocumentNotFoundException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.cache.annotation.CacheEvict;
-import org.springframework.cache.annotation.Cacheable;
 import org.springframework.cache.annotation.Caching;
+import org.springframework.context.event.EventListener;
 import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.security.access.annotation.Secured;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Service;
 
@@ -60,6 +62,8 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem
 
 	private ContentRepository contentRepository;
 
+	private AnswerService answerService;
+
 	private AnswerRepository answerRepository;
 
 	private static final Logger logger = LoggerFactory.getLogger(ContentServiceImpl.class);
@@ -79,6 +83,11 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem
 		this.userService = userService;
 	}
 
+	@Autowired
+	public void setAnswerService(final AnswerService answerService) {
+		this.answerService = answerService;
+	}
+
 	@Override
 	protected void modifyRetrieved(final Content content) {
 		if (content.getFormat() != Content.Format.TEXT && 0 == content.getState().getRound()) {
@@ -246,16 +255,8 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem
 		roomService.update(room);
 	}
 
-	/* TODO: Only evict cache entry for the content's session. This requires some refactoring. */
 	@Override
-	@PreAuthorize("hasPermission(#contentId, 'content', 'owner')")
-	@Caching(evict = {
-			@CacheEvict("answerlists"),
-			@CacheEvict(value = "contents", key = "#contentId"),
-			@CacheEvict(value = "contentlists", allEntries = true),
-			@CacheEvict(value = "lecturecontentlists", allEntries = true /*, condition = "#content.getGroups().contains('lecture')"*/),
-			@CacheEvict(value = "preparationcontentlists", allEntries = true /*, condition = "#content.getGroups().contains('preparation')"*/),
-			@CacheEvict(value = "flashcardcontentlists", allEntries = true /*, condition = "#content.getGroups().contains('flashcard')"*/) })
+	@PreAuthorize("isAuthenticated()")
 	public void delete(final String contentId) {
 		final Content content = get(contentId);
 		if (content == null) {
@@ -263,34 +264,22 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem
 		}
 
 		try {
-			final int count = answerRepository.deleteByContentId(contentId);
 			delete(content);
-			dbLogger.log("delete", "type", "content", "answerCount", count);
 		} catch (final IllegalArgumentException e) {
 			logger.error("Could not delete content {}.", contentId, e);
 		}
 	}
 
-	@PreAuthorize("hasPermission(#session, 'owner')")
-	@Caching(evict = {
-			@CacheEvict(value = "contents", allEntries = true),
-			@CacheEvict(value = "contentlists", key = "#room.getId()"),
-			@CacheEvict(value = "lecturecontentlists", key = "#room.getId()", condition = "'lecture'.equals(#variant)"),
-			@CacheEvict(value = "preparationcontentlists", key = "#room.getId()", condition = "'preparation'.equals(#variant)"),
-			@CacheEvict(value = "flashcardcontentlists", key = "#room.getId()", condition = "'flashcard'.equals(#variant)") })
+	@PreAuthorize("isAuthenticated()")
 	private void deleteBySessionAndVariant(final Room room, final String variant) {
-		final List<String> contentIds;
+		final Iterable<Content> contents;
 		if ("all".equals(variant)) {
-			contentIds = contentRepository.findIdsByRoomId(room.getId());
+			contents = contentRepository.findStubsByRoomId(room.getId());
 		} else {
-			contentIds = contentRepository.findIdsByRoomIdAndVariant(room.getId(), variant);
+			contents = contentRepository.findStubsByRoomIdAndVariant(room.getId(), variant);
 		}
 
-		/* TODO: use EntityService! */
-		final int answerCount = answerRepository.deleteByContentIds(contentIds);
-		final int contentCount = contentRepository.deleteByRoomId(room.getId());
-		dbLogger.log("delete", "type", "question", "questionCount", contentCount);
-		dbLogger.log("delete", "type", "answer", "answerCount", answerCount);
+		delete(contents);
 	}
 
 	@Override
@@ -445,10 +434,8 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem
 		patch(contents, Collections.singletonMap("visible", publish), Content::getState);
 	}
 
-	/* TODO: Split and move answer part to AnswerService */
 	@Override
 	@PreAuthorize("isAuthenticated()")
-	@CacheEvict(value = "answerlists", allEntries = true)
 	public void deleteAllContentsAnswers(final String roomId) {
 		final User user = getCurrentUser();
 		final Room room = roomService.get(roomId);
@@ -459,38 +446,29 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem
 		final List<Content> contents = contentRepository.findByRoomIdAndVariantAndActive(room.getId());
 		resetContentsRoundState(room.getId(), contents);
 		final List<String> contentIds = contents.stream().map(Content::getId).collect(Collectors.toList());
-		/* TODO: use EntityService! */
-		answerRepository.deleteAllAnswersForQuestions(contentIds);
+		answerService.delete(answerRepository.findStubsByContentIds(contentIds));
 	}
 
-	/* TODO: Split and move answer part to AnswerService */
-	/* TODO: Only evict cache entry for the answer's content. This requires some refactoring. */
 	@Override
-	@PreAuthorize("hasPermission(#roomId, 'room', 'owner')")
-	@CacheEvict(value = "answerlists", allEntries = true)
+	@PreAuthorize("isAuthenticated()")
 	public void deleteAllPreparationAnswers(String roomId) {
 		final Room room = roomService.get(roomId);
 
 		final List<Content> contents = contentRepository.findByRoomIdAndVariantAndActive(room.getId(), "preparation");
 		resetContentsRoundState(room.getId(), contents);
 		final List<String> contentIds = contents.stream().map(Content::getId).collect(Collectors.toList());
-		/* TODO: use EntityService! */
-		answerRepository.deleteAllAnswersForQuestions(contentIds);
+		answerService.delete(answerRepository.findStubsByContentIds(contentIds));
 	}
 
-	/* TODO: Split and move answer part to AnswerService */
-	/* TODO: Only evict cache entry for the answer's content. This requires some refactoring. */
 	@Override
-	@PreAuthorize("hasPermission(#roomId, 'room', 'owner')")
-	@CacheEvict(value = "answerlists", allEntries = true)
+	@PreAuthorize("isAuthenticated()")
 	public void deleteAllLectureAnswers(String roomId) {
 		final Room room = roomService.get(roomId);
 
 		final List<Content> contents = contentRepository.findByRoomIdAndVariantAndActive(room.getId(), "lecture");
 		resetContentsRoundState(room.getId(), contents);
 		final List<String> contentIds = contents.stream().map(Content::getId).collect(Collectors.toList());
-		/* TODO: use EntityService! */
-		answerRepository.deleteAllAnswersForQuestions(contentIds);
+		answerService.delete(answerRepository.findStubsByContentIds(contentIds));
 	}
 
 	@Caching(evict = {
@@ -507,4 +485,11 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem
 		}
 		contentRepository.saveAll(contents);
 	}
+
+	@EventListener
+	@Secured({"ROLE_USER", "RUN_AS_SYSTEM"})
+	public void handleRoomDeletion(final BeforeDeletionEvent<Room> event) {
+		final Iterable<Content> contents = contentRepository.findStubsByRoomId(event.getEntity().getId());
+		delete(contents);
+	}
 }
diff --git a/src/main/java/de/thm/arsnova/service/RoomService.java b/src/main/java/de/thm/arsnova/service/RoomService.java
index 1660058b6..a5c676ca0 100644
--- a/src/main/java/de/thm/arsnova/service/RoomService.java
+++ b/src/main/java/de/thm/arsnova/service/RoomService.java
@@ -62,8 +62,6 @@ public interface RoomService extends EntityService<Room> {
 
 	Room updateCreator(String id, String newCreator);
 
-	int[] deleteCascading(Room room);
-
 	ScoreStatistics getLearningProgress(String id, String type, String questionVariant);
 
 	ScoreStatistics getMyLearningProgress(String id, String type, String questionVariant);
diff --git a/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java b/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java
index 7aae870b9..3fe18f97f 100644
--- a/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java
+++ b/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java
@@ -155,20 +155,10 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R
 			long lastActivityBefore = unixTime - guestRoomInactivityThresholdDays * 24 * 60 * 60 * 1000L;
 			int totalCount[] = new int[] {0, 0, 0};
 			List<Room> inactiveRooms = roomRepository.findInactiveGuestRoomsMetadata(lastActivityBefore);
-			for (Room room : inactiveRooms) {
-				int[] count = deleteCascading(room);
-				totalCount[0] += count[0];
-				totalCount[1] += count[1];
-				totalCount[2] += count[2];
-			}
+			delete(inactiveRooms);
 
 			if (!inactiveRooms.isEmpty()) {
 				logger.info("Deleted {} inactive guest rooms.", inactiveRooms.size());
-				dbLogger.log("cleanup", "type", "session",
-						"sessionCount", inactiveRooms.size(),
-						"questionCount", totalCount[0],
-						"answerCount", totalCount[1],
-						"commentCount", totalCount[2]);
 			}
 		}
 	}
@@ -432,25 +422,6 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R
 		throw new UnsupportedOperationException("No longer implemented.");
 	}
 
-	@Override
-	@PreAuthorize("hasPermission(#room, 'owner')")
-	@Caching(evict = {
-			@CacheEvict("rooms"),
-			@CacheEvict(value = "room.id-by-shortid", key = "#room.shortId")
-	})
-	public int[] deleteCascading(final Room room) {
-		int[] count = new int[] {0, 0, 0};
-		List<String> contentIds = contentRepository.findIdsByRoomId(room.getId());
-		count[2] = commentRepository.deleteByRoomId(room.getId());
-		count[1] = answerRepository.deleteByContentIds(contentIds);
-		count[0] = contentRepository.deleteByRoomId(room.getId());
-		delete(room);
-		logger.debug("Deleted room document {} and related data.", room.getId());
-		dbLogger.log("delete", "type", "session", "id", room.getId());
-
-		return count;
-	}
-
 	@Override
 	@PreAuthorize("hasPermission(#id, 'room', 'read')")
 	public ScoreStatistics getLearningProgress(final String id, final String type, final String questionVariant) {
diff --git a/src/main/java/de/thm/arsnova/service/TimerServiceImpl.java b/src/main/java/de/thm/arsnova/service/TimerServiceImpl.java
index 5a4fad25a..9034ccc72 100644
--- a/src/main/java/de/thm/arsnova/service/TimerServiceImpl.java
+++ b/src/main/java/de/thm/arsnova/service/TimerServiceImpl.java
@@ -19,13 +19,16 @@ public class TimerServiceImpl implements TimerService {
 	private UserService userService;
 	private RoomService roomService;
 	private ContentService contentService;
+	private AnswerService answerService;
 	private AnswerRepository answerRepository;
 
 	public TimerServiceImpl(final UserService userService, final RoomService roomService,
-			final ContentService contentService, final AnswerRepository answerRepository) {
+			final ContentService contentService, final AnswerService answerService,
+			final AnswerRepository answerRepository) {
 		this.userService = userService;
 		this.roomService = roomService;
 		this.contentService = contentService;
+		this.answerService = answerService;
 		this.answerRepository = answerRepository;
 	}
 
@@ -109,7 +112,7 @@ public class TimerServiceImpl implements TimerService {
 		}
 
 		resetRoundManagementState(content);
-		answerRepository.deleteByContentId(content.getId());
+		answerRepository.findStubsByContentId(content.getId());
 		contentService.update(content);
 	}
 
-- 
GitLab