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