From 606f12d96c741cb26e7136945714b81c5684ea56 Mon Sep 17 00:00:00 2001 From: Daniel Gerhardt <code@dgerhardt.net> Date: Sat, 27 Jul 2019 09:22:56 +0200 Subject: [PATCH] Refactor handling of content groups Content groups are now a separate entity with their own repository and service and endpoint. A content group can no longer be set when creating a new content. Instead the new API endpoint has to be used to add the content to a group. --- .../thm/arsnova/config/PersistenceConfig.java | 7 + .../arsnova/controller/RoomController.java | 20 ++- .../de/thm/arsnova/model/ContentGroup.java | 122 ++++++++++++++++++ src/main/java/de/thm/arsnova/model/Room.java | 101 +-------------- .../persistence/ContentGroupRepository.java | 29 +++++ .../CouchDbContentGroupRepository.java | 56 ++++++++ .../ApplicationPermissionEvaluator.java | 33 +++++ .../arsnova/service/ContentGroupService.java | 92 +++++++++++++ .../arsnova/service/ContentServiceImpl.java | 82 ++++-------- .../thm/arsnova/service/RoomServiceImpl.java | 28 ---- .../resources/couchdb/ContentGroup.design.js | 22 ++++ .../arsnova/config/TestPersistanceConfig.java | 6 + 12 files changed, 411 insertions(+), 187 deletions(-) create mode 100644 src/main/java/de/thm/arsnova/model/ContentGroup.java create mode 100644 src/main/java/de/thm/arsnova/persistence/ContentGroupRepository.java create mode 100644 src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentGroupRepository.java create mode 100644 src/main/java/de/thm/arsnova/service/ContentGroupService.java create mode 100644 src/main/resources/couchdb/ContentGroup.design.js diff --git a/src/main/java/de/thm/arsnova/config/PersistenceConfig.java b/src/main/java/de/thm/arsnova/config/PersistenceConfig.java index 1f831776d..f8e6bdf55 100644 --- a/src/main/java/de/thm/arsnova/config/PersistenceConfig.java +++ b/src/main/java/de/thm/arsnova/config/PersistenceConfig.java @@ -32,6 +32,7 @@ import de.thm.arsnova.config.properties.CouchDbProperties; import de.thm.arsnova.model.serialization.CouchDbObjectMapperFactory; import de.thm.arsnova.persistence.AnswerRepository; import de.thm.arsnova.persistence.CommentRepository; +import de.thm.arsnova.persistence.ContentGroupRepository; import de.thm.arsnova.persistence.ContentRepository; import de.thm.arsnova.persistence.LogEntryRepository; import de.thm.arsnova.persistence.MotdRepository; @@ -41,6 +42,7 @@ import de.thm.arsnova.persistence.StatisticsRepository; import de.thm.arsnova.persistence.UserRepository; import de.thm.arsnova.persistence.couchdb.CouchDbAnswerRepository; import de.thm.arsnova.persistence.couchdb.CouchDbCommentRepository; +import de.thm.arsnova.persistence.couchdb.CouchDbContentGroupRepository; import de.thm.arsnova.persistence.couchdb.CouchDbContentRepository; import de.thm.arsnova.persistence.couchdb.CouchDbLogEntryRepository; import de.thm.arsnova.persistence.couchdb.CouchDbMotdRepository; @@ -140,6 +142,11 @@ public class PersistenceConfig { return new CouchDbContentRepository(couchDbConnector(), false); } + @Bean + public ContentGroupRepository contentGroupRepository() throws Exception { + return new CouchDbContentGroupRepository(couchDbConnector(), false); + } + @Bean public AnswerRepository answerRepository() throws Exception { return new CouchDbAnswerRepository(couchDbConnector(), false); diff --git a/src/main/java/de/thm/arsnova/controller/RoomController.java b/src/main/java/de/thm/arsnova/controller/RoomController.java index 67eb68cba..5357089b8 100644 --- a/src/main/java/de/thm/arsnova/controller/RoomController.java +++ b/src/main/java/de/thm/arsnova/controller/RoomController.java @@ -22,12 +22,15 @@ import java.util.Set; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import de.thm.arsnova.model.ContentGroup; import de.thm.arsnova.model.Room; +import de.thm.arsnova.service.ContentGroupService; import de.thm.arsnova.service.RoomService; @RestController @@ -36,12 +39,16 @@ public class RoomController extends AbstractEntityController<Room> { protected static final String REQUEST_MAPPING = "/room"; private static final String GET_MODERATORS_MAPPING = DEFAULT_ID_MAPPING + "/moderator"; private static final String MODERATOR_MAPPING = DEFAULT_ID_MAPPING + "/moderator/{userId}"; + private static final String CONTENTGROUP_MAPPING = DEFAULT_ID_MAPPING + "/contentgroup/{groupName}"; + private static final String CONTENTGROUP_ADD_MAPPING = CONTENTGROUP_MAPPING + "/{contentId}"; private RoomService roomService; + private ContentGroupService contentGroupService; - public RoomController(final RoomService roomService) { + public RoomController(final RoomService roomService, final ContentGroupService contentGroupService) { super(roomService); this.roomService = roomService; + this.contentGroupService = contentGroupService; } @Override @@ -78,4 +85,15 @@ public class RoomController extends AbstractEntityController<Room> { room.getModerators().removeIf(m -> m.getUserId().equals(userId)); roomService.update(room); } + + @GetMapping(CONTENTGROUP_MAPPING) + public ContentGroup getContentGroup(@PathVariable final String id, @PathVariable final String groupName) { + return contentGroupService.getByRoomIdAndName(id, groupName); + } + + @PostMapping(CONTENTGROUP_ADD_MAPPING) + public void addContentToGroup(@PathVariable final String id, @PathVariable final String groupName, + @RequestBody final String contentId) { + contentGroupService.addContentToGroup(id, groupName, contentId); + } } diff --git a/src/main/java/de/thm/arsnova/model/ContentGroup.java b/src/main/java/de/thm/arsnova/model/ContentGroup.java new file mode 100644 index 000000000..25ad8733c --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/ContentGroup.java @@ -0,0 +1,122 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2019 The ARSnova Team and Contributors + * + * 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.model; + +import com.fasterxml.jackson.annotation.JsonView; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import org.springframework.core.style.ToStringCreator; + +import de.thm.arsnova.model.serialization.View; + +public class ContentGroup extends Entity { + @NotEmpty + private String roomId; + + @NotBlank + private String name; + + private Set<String> contentIds; + private boolean autoSort; + + public ContentGroup() { + + } + + public ContentGroup(final String roomId, final String name) { + this.roomId = roomId; + this.name = name; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getRoomId() { + return roomId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setRoomId(final String roomId) { + this.roomId = roomId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getName() { + return this.name; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setName(final String name) { + this.name = name; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Set<String> getContentIds() { + if (contentIds == null) { + contentIds = new HashSet<>(); + } + + return contentIds; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setContentIds(final Set<String> contentIds) { + this.contentIds = contentIds; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isAutoSort() { + return autoSort; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setAutoSort(final boolean autoSort) { + this.autoSort = autoSort; + } + + @Override + public int hashCode() { + return Objects.hash(name, contentIds, autoSort); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ContentGroup that = (ContentGroup) o; + + return autoSort == that.autoSort + && Objects.equals(name, that.name) + && Objects.equals(contentIds, that.contentIds); + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("name", name) + .append("contentIds", contentIds) + .append("autoSort", autoSort) + .toString(); + } +} diff --git a/src/main/java/de/thm/arsnova/model/Room.java b/src/main/java/de/thm/arsnova/model/Room.java index 689eae0a8..cf5f69e31 100644 --- a/src/main/java/de/thm/arsnova/model/Room.java +++ b/src/main/java/de/thm/arsnova/model/Room.java @@ -23,8 +23,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty; import org.springframework.core.style.ToStringCreator; @@ -32,77 +30,6 @@ import org.springframework.core.style.ToStringCreator; import de.thm.arsnova.model.serialization.View; public class Room extends Entity { - public static class ContentGroup { - @NotBlank - private String name; - - private Set<String> contentIds; - private boolean autoSort; - - @JsonView({View.Persistence.class, View.Public.class}) - public String getName() { - return this.name; - } - - @JsonView({View.Persistence.class, View.Public.class}) - public void setName(final String name) { - this.name = name; - } - - @JsonView({View.Persistence.class, View.Public.class}) - public Set<String> getContentIds() { - if (contentIds == null) { - contentIds = new HashSet<>(); - } - - return contentIds; - } - - @JsonView({View.Persistence.class, View.Public.class}) - public void setContentIds(final Set<String> contentIds) { - this.contentIds = contentIds; - } - - @JsonView({View.Persistence.class, View.Public.class}) - public boolean isAutoSort() { - return autoSort; - } - - @JsonView({View.Persistence.class, View.Public.class}) - public void setAutoSort(final boolean autoSort) { - this.autoSort = autoSort; - } - - @Override - public int hashCode() { - return Objects.hash(name, contentIds, autoSort); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final ContentGroup that = (ContentGroup) o; - - return autoSort == that.autoSort - && Objects.equals(name, that.name) - && Objects.equals(contentIds, that.contentIds); - } - - @Override - public String toString() { - return new ToStringCreator(this) - .append("name", name) - .append("contentIds", contentIds) - .append("autoSort", autoSort) - .toString(); - } - } - public static class Moderator { public enum Role { EDITING_MODERATOR, @@ -468,7 +395,6 @@ public class Room extends Entity { private String description; private boolean closed; - private Set<ContentGroup> contentGroups; private Set<Moderator> moderators; private Settings settings; private Author author; @@ -537,28 +463,6 @@ public class Room extends Entity { this.closed = closed; } - @JsonView({View.Persistence.class, View.Public.class}) - public Set<ContentGroup> getContentGroups() { - if (contentGroups == null) { - contentGroups = new HashSet<>(); - } - - return contentGroups; - } - - public Map<String, ContentGroup> getContentGroupsAsMap() { - return getContentGroups().stream().collect(Collectors.toMap(ContentGroup::getName, Function.identity())); - } - - @JsonView({View.Persistence.class, View.Public.class}) - public void setContentGroups(final Set<ContentGroup> contentGroups) { - this.contentGroups = contentGroups; - } - - public void setContentGroupsFromMap(final Map<String, ContentGroup> groups) { - this.contentGroups = new HashSet<>(groups.values()); - } - @JsonView(View.Persistence.class) public Set<Moderator> getModerators() { if (moderators == null) { @@ -641,8 +545,8 @@ public class Room extends Entity { * * <p> * The following fields of <tt>Room</tt> are excluded from equality checks: - * {@link #contentGroups}, {@link #settings}, {@link #author}, {@link #poolProperties}, {@link #extensions}, - * {@link #attachments}, {@link #statistics}. + * {@link #settings}, {@link #author}, {@link #poolProperties}, {@link #extensions}, {@link #attachments}, + * {@link #statistics}. * </p> */ @Override @@ -672,7 +576,6 @@ public class Room extends Entity { .append("abbreviation", abbreviation) .append("description", description) .append("closed", closed) - .append("contentGroups", contentGroups) .append("settings", settings) .append("author", author) .append("poolProperties", poolProperties) diff --git a/src/main/java/de/thm/arsnova/persistence/ContentGroupRepository.java b/src/main/java/de/thm/arsnova/persistence/ContentGroupRepository.java new file mode 100644 index 000000000..6ee6a87b0 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/ContentGroupRepository.java @@ -0,0 +1,29 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2019 The ARSnova Team and Contributors + * + * 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.persistence; + +import java.util.List; + +import de.thm.arsnova.model.ContentGroup; + +public interface ContentGroupRepository extends CrudRepository<ContentGroup, String> { + ContentGroup findByRoomIdAndName(String roomId, String name); + + List<ContentGroup> findByRoomId(String roomId); +} diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentGroupRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentGroupRepository.java new file mode 100644 index 000000000..8edebb3ec --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentGroupRepository.java @@ -0,0 +1,56 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2019 The ARSnova Team and Contributors + * + * 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.persistence.couchdb; + +import java.util.List; +import org.ektorp.ComplexKey; +import org.ektorp.CouchDbConnector; + +import de.thm.arsnova.model.ContentGroup; +import de.thm.arsnova.persistence.ContentGroupRepository; + +public class CouchDbContentGroupRepository extends CouchDbCrudRepository<ContentGroup> + implements ContentGroupRepository { + public CouchDbContentGroupRepository(final CouchDbConnector db, final boolean createIfNotExists) { + super(ContentGroup.class, db, "by_id", createIfNotExists); + } + + @Override + public ContentGroup findByRoomIdAndName(final String roomId, final String name) { + final List<ContentGroup> contentGroupList = db.queryView(createQuery("by_roomid_name") + .key(ComplexKey.of(roomId, name)) + .includeDocs(true) + .reduce(false), + ContentGroup.class); + + return !contentGroupList.isEmpty() ? contentGroupList.get(0) : null; + } + + @Override + public List<ContentGroup> findByRoomId(final String roomId) { + final List<ContentGroup> contentGroups = db.queryView(createQuery("by_roomid_name") + .startKey(ComplexKey.of(roomId)) + .endKey(ComplexKey.of(roomId, ComplexKey.emptyObject())) + .includeDocs(true) + .reduce(false), + ContentGroup.class); + + return contentGroups; + } +} diff --git a/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java b/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java index d91e45747..66a50e5be 100644 --- a/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java +++ b/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java @@ -35,11 +35,13 @@ import de.thm.arsnova.config.properties.SecurityProperties; import de.thm.arsnova.model.Answer; import de.thm.arsnova.model.Comment; import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.ContentGroup; import de.thm.arsnova.model.Motd; import de.thm.arsnova.model.Room; import de.thm.arsnova.model.UserProfile; import de.thm.arsnova.persistence.AnswerRepository; import de.thm.arsnova.persistence.CommentRepository; +import de.thm.arsnova.persistence.ContentGroupRepository; import de.thm.arsnova.persistence.ContentRepository; import de.thm.arsnova.persistence.MotdRepository; import de.thm.arsnova.persistence.RoomRepository; @@ -67,6 +69,9 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { @Autowired private ContentRepository contentRepository; + @Autowired + ContentGroupRepository contentGroupRepository; + @Autowired private AnswerRepository answerRepository; @@ -96,6 +101,8 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { && hasRoomPermission(userId, ((Room) targetDomainObject), permission.toString())) || (targetDomainObject instanceof Content && hasContentPermission(userId, ((Content) targetDomainObject), permission.toString())) + || (targetDomainObject instanceof ContentGroup + && hasContentGroupPermission(userId, ((ContentGroup) targetDomainObject), permission.toString())) || (targetDomainObject instanceof Answer && hasAnswerPermission(userId, ((Answer) targetDomainObject), permission.toString())) || (targetDomainObject instanceof Comment @@ -132,6 +139,10 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { case "content": final Content targetContent = contentRepository.findOne(targetId.toString()); return targetContent != null && hasContentPermission(userId, targetContent, permission.toString()); + case "contentgroup": + final ContentGroup targetContentGroup = contentGroupRepository.findOne(targetId.toString()); + return targetContentGroup != null + && hasContentGroupPermission(userId, targetContentGroup, permission.toString()); case "answer": final Answer targetAnswer = answerRepository.findOne(targetId.toString()); return targetAnswer != null && hasAnswerPermission(userId, targetAnswer, permission.toString()); @@ -211,6 +222,28 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { } } + private boolean hasContentGroupPermission( + final String userId, + final ContentGroup targetContentGroup, + final String permission) { + final Room room = roomRepository.findOne(targetContentGroup.getRoomId()); + if (room == null) { + return false; + } + + switch (permission) { + case "read": + return !room.isClosed() || hasUserIdRoomModeratingPermission(room, userId); + case "create": + case "update": + case "delete": + return room.getOwnerId().equals(userId) + || hasUserIdRoomModeratorRole(room, userId, Room.Moderator.Role.EDITING_MODERATOR); + default: + return false; + } + } + private boolean hasAnswerPermission( final String userId, final Answer targetAnswer, diff --git a/src/main/java/de/thm/arsnova/service/ContentGroupService.java b/src/main/java/de/thm/arsnova/service/ContentGroupService.java new file mode 100644 index 000000000..a227a984f --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/ContentGroupService.java @@ -0,0 +1,92 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2019 The ARSnova Team and Contributors + * + * 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.service; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +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.stereotype.Service; +import org.springframework.validation.Validator; + +import de.thm.arsnova.event.BeforeDeletionEvent; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.ContentGroup; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.persistence.ContentGroupRepository; + +@Service +public class ContentGroupService extends DefaultEntityServiceImpl<ContentGroup> { + private ContentGroupRepository contentGroupRepository; + private ContentService contentService; + + public ContentGroupService( + final ContentGroupRepository repository, + final ContentService contentService, + @Qualifier("defaultJsonMessageConverter") + final MappingJackson2HttpMessageConverter jackson2HttpMessageConverter, + final Validator validator) { + super(ContentGroup.class, repository, jackson2HttpMessageConverter.getObjectMapper(), validator); + this.contentGroupRepository = repository; + this.contentService = contentService; + } + + public ContentGroup getByRoomIdAndName(final String roomId, final String name) { + return contentGroupRepository.findByRoomIdAndName(roomId, name); + } + + public List<ContentGroup> getByRoomId(final String roomId) { + return contentGroupRepository.findByRoomId(roomId); + } + + public void addContentToGroup(final String roomId, final String groupName, final String contentId) { + ContentGroup contentGroup = getByRoomIdAndName(roomId, groupName); + if (contentGroup == null) { + contentGroup = new ContentGroup(roomId, groupName); + contentGroup.getContentIds().add(contentId); + create(contentGroup); + } else { + contentGroup.getContentIds().add(contentId); + update(contentGroup); + } + } + + public void updateContentGroup(final ContentGroup contentGroup) { + if (contentGroup.getContentIds().isEmpty()) { + delete(contentGroup); + } else { + final Set<String> contentIds = StreamSupport.stream( + contentService.get(contentGroup.getContentIds()).spliterator(), false) + .filter(c -> c.getRoomId().equals(contentGroup.getRoomId())) + .map(Content::getId).collect(Collectors.toSet()); + update(contentGroup); + } + } + + @EventListener + @Secured({"ROLE_USER", "RUN_AS_SYSTEM"}) + public void handleRoomDeletion(final BeforeDeletionEvent<Room> event) { + final Iterable<ContentGroup> contentGroups = contentGroupRepository.findByRoomId(event.getEntity().getId()); + delete(contentGroups); + } +} diff --git a/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java b/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java index 4321993f8..78208b53d 100644 --- a/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java +++ b/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -42,8 +41,8 @@ import org.springframework.validation.Validator; import de.thm.arsnova.event.BeforeDeletionEvent; import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.ContentGroup; import de.thm.arsnova.model.Room; -import de.thm.arsnova.model.Room.ContentGroup; import de.thm.arsnova.persistence.AnswerRepository; import de.thm.arsnova.persistence.ContentRepository; import de.thm.arsnova.persistence.LogEntryRepository; @@ -64,6 +63,8 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem private ContentRepository contentRepository; + private ContentGroupService contentGroupService; + private AnswerService answerService; private AnswerRepository answerRepository; @@ -92,16 +93,17 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem this.answerService = answerService; } + @Autowired + public void setContentGroupService(final ContentGroupService contentGroupService) { + this.contentGroupService = contentGroupService; + } + @Override protected void modifyRetrieved(final Content content) { if (content.getFormat() != Content.Format.TEXT && 0 == content.getState().getRound()) { /* needed for legacy questions whose piRound property has not been set */ content.getState().setRound(1); } - - final Room room = roomService.get(content.getRoomId()); - content.setGroups(room.getContentGroups().stream() - .map(Room.ContentGroup::getName).filter(g -> !g.isEmpty()).collect(Collectors.toSet())); } /* FIXME: caching */ @@ -120,13 +122,8 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem @Override public Iterable<Content> getByRoomIdAndGroup(final String roomId, final String group) { - final Room room = roomService.get(roomId); - Room.ContentGroup contentGroup = null; - for (final Room.ContentGroup cg : room.getContentGroups()) { - if (cg.getName().equals(group)) { - contentGroup = cg; - } - } + final ContentGroup contentGroup = contentGroupService.getByRoomIdAndName(roomId, group); + if (contentGroup == null) { throw new NotFoundException("Content group does not exist."); } @@ -142,13 +139,8 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem @Override public int countByRoomIdAndGroup(final String roomId, final String group) { - final Room room = roomService.get(roomId); - Room.ContentGroup contentGroup = null; - for (final Room.ContentGroup cg : room.getContentGroups()) { - if (cg.getName().equals(group)) { - contentGroup = cg; - } - } + final ContentGroup contentGroup = contentGroupService.getByRoomIdAndName(roomId, group); + if (contentGroup == null) { throw new NotFoundException("Content group does not exist."); } @@ -179,22 +171,6 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem */ } - @Override - protected void finalizeCreate(final Content content) { - /* Update content groups of room */ - final Room room = roomService.get(content.getRoomId()); - final Map<String, Room.ContentGroup> groups = room.getContentGroupsAsMap(); - for (final String groupName : content.getGroups()) { - final Room.ContentGroup group = groups.getOrDefault(groupName, new Room.ContentGroup()); - groups.put(groupName, group); - group.getContentIds().add(content.getId()); - group.setName(groupName); - group.setAutoSort(true); - } - room.setContentGroupsFromMap(groups); - roomService.update(room); - } - @Override protected void prepareUpdate(final Content content) { final User user = userService.getCurrentUser(); @@ -220,25 +196,6 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem @Override protected void finalizeUpdate(final Content content) { - /* Update content groups of room */ - final Room room = roomService.get(content.getRoomId()); - final Set<String> contentsGroupNames = content.getGroups(); - final Set<String> allGroupNames = new HashSet<>(contentsGroupNames); - final Map<String, Room.ContentGroup> groups = room.getContentGroupsAsMap(); - allGroupNames.addAll(groups.keySet()); - for (final String groupName : allGroupNames) { - final Room.ContentGroup group = groups.getOrDefault(groupName, new Room.ContentGroup()); - if (contentsGroupNames.contains(groupName)) { - group.getContentIds().add(content.getId()); - group.setName(groupName); - group.setAutoSort(true); - } else { - group.getContentIds().remove(content.getId()); - } - } - room.setContentGroupsFromMap(groups); - roomService.update(room); - /* TODO: not sure yet how to refactor this code - we need access to the old and new entity if (!oldContent.getState().isVisible() && content.getState().isVisible()) { final UnlockQuestionEvent event = new UnlockQuestionEvent(this, room, content); @@ -252,11 +209,18 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem @Override protected void prepareDelete(final Content content) { - final Room room = roomService.get(content.getRoomId()); - for (final ContentGroup group : room.getContentGroups()) { - group.getContentIds().remove(content.getId()); + final List<ContentGroup> contentGroups = contentGroupService.getByRoomId(content.getRoomId()); + for (final ContentGroup contentGroup : contentGroups) { + final Set<String> ids = contentGroup.getContentIds(); + if (ids.contains(content.getId())) { + ids.remove(content.getId()); + if (!ids.isEmpty()) { + contentGroupService.update(contentGroup); + } else { + contentGroupService.delete(contentGroup); + } + } } - roomService.update(room); } @Override diff --git a/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java b/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java index 0ddfa42fb..2ea4ae30e 100644 --- a/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java +++ b/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java @@ -23,9 +23,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -167,32 +165,6 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R } } - /** - * Adds a default content group with contents that have no other content group assigned. - * - * @param room The Room to be modified - */ - @Override - protected void modifyRetrieved(final Room room) { - // creates a set from all room content groups - final Set<String> cidsWithGroup = room.getContentGroups().stream() - .map(Room.ContentGroup::getContentIds) - .flatMap(ids -> ids.stream()) - .collect(Collectors.toSet()); - - final Set<String> cids = new HashSet<>(contentRepository.findIdsByRoomId(room.getId())); - cids.removeAll(cidsWithGroup); - - if (!cids.isEmpty()) { - final Set<Room.ContentGroup> cgs = room.getContentGroups(); - final Room.ContentGroup defaultGroup = new Room.ContentGroup(); - defaultGroup.setContentIds(cids); - defaultGroup.setAutoSort(true); - defaultGroup.setName(""); - cgs.add(defaultGroup); - } - } - @Override public Room join(final String id, final UUID socketId) { final Room room = null != id ? get(id) : null; diff --git a/src/main/resources/couchdb/ContentGroup.design.js b/src/main/resources/couchdb/ContentGroup.design.js new file mode 100644 index 000000000..8592b2926 --- /dev/null +++ b/src/main/resources/couchdb/ContentGroup.design.js @@ -0,0 +1,22 @@ +var designDoc = { + "_id": "_design/ContentGroup", + "language": "javascript", + "views": { + "by_id": { + "map": function (doc) { + if (doc.type === "ContentGroup") { + emit(doc._id, {_rev: doc._rev}); + } + }, + "reduce": "_count" + }, + "by_roomid_name": { + "map": function (doc) { + if (doc.type === "ContentGroup") { + emit([doc.roomId, doc.name], {_rev: doc._rev}); + } + }, + "reduce": "_count" + } + } +}; diff --git a/src/test/java/de/thm/arsnova/config/TestPersistanceConfig.java b/src/test/java/de/thm/arsnova/config/TestPersistanceConfig.java index 92e5af312..89974031a 100644 --- a/src/test/java/de/thm/arsnova/config/TestPersistanceConfig.java +++ b/src/test/java/de/thm/arsnova/config/TestPersistanceConfig.java @@ -26,6 +26,7 @@ import org.springframework.context.annotation.Profile; import de.thm.arsnova.persistence.AnswerRepository; import de.thm.arsnova.persistence.AttachmentRepository; import de.thm.arsnova.persistence.CommentRepository; +import de.thm.arsnova.persistence.ContentGroupRepository; import de.thm.arsnova.persistence.ContentRepository; import de.thm.arsnova.persistence.LogEntryRepository; import de.thm.arsnova.persistence.MotdRepository; @@ -62,6 +63,11 @@ public class TestPersistanceConfig { return Mockito.mock(ContentRepository.class); } + @Bean + public ContentGroupRepository contentGroupRepository() { + return Mockito.mock(ContentGroupRepository.class); + } + @Bean public AnswerRepository answerRepository() { return Mockito.mock(AnswerRepository.class); -- GitLab