1

Merge pull request #16 from ChUrl/FEATURE-caching

Feature caching
This commit is contained in:
Christoph
2020-04-16 01:33:07 +02:00
committed by GitHub
31 changed files with 286 additions and 484 deletions

View File

@ -1,6 +1,5 @@
package mops.gruppen2.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@ -16,7 +15,6 @@ import java.util.Collections;
@Profile("dev")
@Configuration
@EnableCaching
@EnableSwagger2
public class SwaggerConfig {

View File

@ -11,6 +11,7 @@ import mops.gruppen2.domain.exception.BadArgumentException;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.exception.IdMismatchException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.infrastructure.GroupCache;
import java.util.UUID;
@ -31,7 +32,6 @@ import java.util.UUID;
@NoArgsConstructor // Lombok needs a default constructor in the base class
public abstract class Event {
@JsonProperty("groupid")
protected UUID groupid;
@ -60,7 +60,7 @@ public abstract class Event {
this.version = version;
}
public Group apply(Group group) throws EventException {
public void apply(Group group, GroupCache cache) throws EventException {
log.trace("Event wird angewendet:\t{}", this);
if (version == 0) {
@ -68,23 +68,32 @@ public abstract class Event {
}
checkGroupIdMatch(group.getId());
group.update(version);
group.updateVersion(version);
applyEvent(group);
return group;
updateCache(cache, group);
}
private void checkGroupIdMatch(UUID groupId) throws IdMismatchException {
private void checkGroupIdMatch(UUID groupid) throws IdMismatchException {
// CreateGroupEvents müssen die Id erst initialisieren
if (this instanceof CreateGroupEvent) {
return;
}
if (!groupid.equals(groupId)) {
if (!this.groupid.equals(groupid)) {
throw new IdMismatchException("Das Event gehört zu einer anderen Gruppe");
}
}
private void updateCache(GroupCache cache, Group group) {
if (this instanceof CreateGroupEvent) {
cache.put(group);
}
if (this instanceof DestroyGroupEvent) {
cache.remove(group);
}
}
protected abstract void applyEvent(Group group) throws EventException;
@JsonIgnore

View File

@ -10,14 +10,14 @@ import mops.gruppen2.domain.exception.LastAdminException;
import mops.gruppen2.domain.exception.NoAccessException;
import mops.gruppen2.domain.exception.UserAlreadyExistsException;
import mops.gruppen2.domain.exception.UserNotFoundException;
import mops.gruppen2.domain.helper.CommonHelper;
import mops.gruppen2.domain.helper.ValidationHelper;
import mops.gruppen2.domain.model.group.wrapper.Body;
import mops.gruppen2.domain.model.group.wrapper.Description;
import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.domain.model.group.wrapper.Link;
import mops.gruppen2.domain.model.group.wrapper.Parent;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.domain.service.helper.CommonHelper;
import mops.gruppen2.domain.service.helper.ValidationHelper;
import javax.validation.Valid;
import java.time.LocalDateTime;
@ -73,6 +73,7 @@ public class Group {
// ####################################### Members ###########################################
public List<User> getMembers() {
return SortHelper.sortByMemberRole(new ArrayList<>(memberships.values())).stream()
.map(Membership::getUser)
@ -210,6 +211,10 @@ public class Group {
return type == Type.LECTURE;
}
public boolean hasParent() {
return !parent.isEmpty();
}
// ######################################## Setters ##########################################
@ -262,7 +267,7 @@ public class Group {
this.link = link;
}
public void update(long version) throws IdMismatchException {
public void updateVersion(long version) throws IdMismatchException {
meta = meta.setVersion(version);
}
@ -310,4 +315,8 @@ public class Group {
+ (meta == null ? "meta: null" : meta.toString())
+ ")";
}
public static Group EMPTY() {
return new Group();
}
}

View File

@ -56,4 +56,8 @@ public class User {
public String format() {
return givenname + " " + familyname;
}
public boolean isMember(Group group) {
return group.getMembers().contains(this);
}
}

View File

@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.ToString;
import lombok.Value;
import mops.gruppen2.domain.helper.CommonHelper;
import mops.gruppen2.domain.service.helper.CommonHelper;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

View File

@ -3,39 +3,26 @@ package mops.gruppen2.domain.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.aspect.annotation.TraceMethodCalls;
import mops.gruppen2.domain.event.AddMemberEvent;
import mops.gruppen2.domain.event.CreateGroupEvent;
import mops.gruppen2.domain.event.Event;
import mops.gruppen2.domain.event.EventType;
import mops.gruppen2.domain.event.SetTypeEvent;
import mops.gruppen2.domain.exception.BadPayloadException;
import mops.gruppen2.domain.exception.InvalidInviteException;
import mops.gruppen2.domain.helper.CommonHelper;
import mops.gruppen2.domain.helper.JsonHelper;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.domain.service.helper.JsonHelper;
import mops.gruppen2.persistance.EventRepository;
import mops.gruppen2.persistance.dto.EventDTO;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static mops.gruppen2.domain.event.EventType.CREATEGROUP;
import static mops.gruppen2.domain.event.EventType.DESTROYGROUP;
import static mops.gruppen2.domain.event.EventType.SETLINK;
import static mops.gruppen2.domain.event.EventType.SETTYPE;
import static mops.gruppen2.domain.helper.CommonHelper.eventTypesToString;
import static mops.gruppen2.domain.helper.CommonHelper.uuidsToString;
import static mops.gruppen2.domain.service.helper.CommonHelper.eventTypesToString;
@Log4j2
@RequiredArgsConstructor
@Service
@TraceMethodCalls
public class EventStoreService {
private final EventRepository eventStore;
@ -77,12 +64,6 @@ public class EventStoreService {
//########################################### DTOs ###########################################
private static List<EventDTO> getDTOsFromEvents(List<Event> events) {
return events.stream()
.map(EventStoreService::getDTOFromEvent)
.collect(Collectors.toList());
}
/**
* Erzeugt aus einem Event Objekt ein EventDTO Objekt.
*
@ -132,81 +113,6 @@ public class EventStoreService {
// ######################################## QUERIES ##########################################
List<Event> findGroupEvents(UUID groupId) {
return getEventsFromDTOs(eventStore.findEventDTOsByGroup(Collections.singletonList(groupId.toString())));
}
/**
* Sucht alle Events, welche zu einer der übergebenen Gruppen gehören.
*
* @param groupIds Liste an IDs
*
* @return Liste an Events
*/
List<Event> findGroupEvents(List<UUID> groupIds) {
List<EventDTO> eventDTOS = new ArrayList<>();
for (UUID groupId : groupIds) {
eventDTOS.addAll(eventStore.findEventDTOsByGroup(Collections.singletonList(groupId.toString())));
}
return getEventsFromDTOs(eventDTOS);
}
/**
* Findet alle Events zu Gruppen, welche seit dem neuen Status verändert wurden.
*
* @param status Die Id des zuletzt gespeicherten Events
*
* @return Liste von neuen und alten Events
*/
List<UUID> findChangedGroups(long status) {
List<String> changedGroupIds = eventStore.findGroupIdsWhereEventIdGreaterThanStatus(status);
log.debug("Seit Event {} haben sich {} Gruppen geändert!", status, changedGroupIds.size());
return CommonHelper.stringsToUUID(changedGroupIds);
}
/**
* Liefert Gruppen-Ids von existierenden (ungelöschten) Gruppen.
*
* @return GruppenIds (UUID) als Liste
*/
List<UUID> findExistingGroupIds() {
List<Event> createEvents = findLatestEventsFromGroupsByType(CREATEGROUP,
DESTROYGROUP);
return createEvents.stream()
.filter(event -> event instanceof CreateGroupEvent)
.map(Event::getGroupid)
.collect(Collectors.toList());
}
public List<UUID> findPublicGroupIds() {
List<UUID> groups = findExistingGroupIds();
List<Event> typeEvents = findLatestEventsFromGroupsByType(SETTYPE);
typeEvents.removeIf(event -> ((SetTypeEvent) event).getType() == Type.PRIVATE);
typeEvents.removeIf(event -> !groups.contains(event.getGroupid()));
return typeEvents.stream()
.map(Event::getGroupid)
.collect(Collectors.toList());
}
public List<UUID> findLectureGroupIds() {
List<UUID> groups = findExistingGroupIds();
List<Event> typeEvents = findLatestEventsFromGroupsByType(SETTYPE);
typeEvents.removeIf(event -> ((SetTypeEvent) event).getType() != Type.LECTURE);
typeEvents.removeIf(event -> !groups.contains(event.getGroupid()));
return typeEvents.stream()
.map(Event::getGroupid)
.collect(Collectors.toList());
}
/**
* Liefert Gruppen-Ids von existierenden (ungelöschten) Gruppen, in welchen der User teilnimmt.
*
@ -234,20 +140,6 @@ public class EventStoreService {
.collect(Collectors.toList());
}
public UUID findGroupByLink(String link) {
List<Event> groupEvents = findEventsByType(eventTypesToString(SETLINK));
if (groupEvents.size() > 1) {
throw new InvalidInviteException("Es existieren mehrere Gruppen mit demselben Link.");
}
if (groupEvents.isEmpty()) {
throw new InvalidInviteException("Link nicht gefunden.");
}
return groupEvents.get(0).getGroupid();
}
// #################################### SIMPLE QUERIES #######################################
@ -262,23 +154,10 @@ public class EventStoreService {
return eventStore.findMaxEventId();
} catch (NullPointerException e) {
log.debug("Keine Events vorhanden!");
return 1;
return 0;
}
}
List<Event> findEventsByType(String... types) {
return getEventsFromDTOs(eventStore.findEventDTOsByType(Arrays.asList(types)));
}
List<Event> findEventsByType(String type) {
return getEventsFromDTOs(eventStore.findEventDTOsByType(Collections.singletonList(type)));
}
List<Event> findEventsByGroupAndType(List<UUID> groupIds, String... types) {
return getEventsFromDTOs(eventStore.findEventDTOsByGroupAndType(uuidsToString(groupIds),
Arrays.asList(types)));
}
/**
* Sucht zu jeder Gruppe das letzte Add- oder DeleteUserEvent heraus, welches den übergebenen User betrifft.
*
@ -301,4 +180,12 @@ public class EventStoreService {
private List<Event> findLatestEventsFromGroupsByType(EventType... types) {
return getEventsFromDTOs(eventStore.findLatestEventDTOsPartitionedByGroupByType(Arrays.asList(eventTypesToString(types))));
}
public List<Event> findAllEvents() {
return getEventsFromDTOs(eventStore.findAllEvents());
}
public List<Event> findNewEvents(long version, long maxid) {
return getEventsFromDTOs(eventStore.findNewEvents(version, maxid));
}
}

View File

@ -2,7 +2,6 @@ package mops.gruppen2.domain.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.aspect.annotation.TraceMethodCalls;
import mops.gruppen2.domain.event.AddMemberEvent;
import mops.gruppen2.domain.event.CreateGroupEvent;
import mops.gruppen2.domain.event.DestroyGroupEvent;
@ -16,7 +15,6 @@ import mops.gruppen2.domain.event.SetTitleEvent;
import mops.gruppen2.domain.event.SetTypeEvent;
import mops.gruppen2.domain.event.UpdateRoleEvent;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.helper.ValidationHelper;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Role;
import mops.gruppen2.domain.model.group.Type;
@ -26,6 +24,8 @@ import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.domain.model.group.wrapper.Link;
import mops.gruppen2.domain.model.group.wrapper.Parent;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.domain.service.helper.ValidationHelper;
import mops.gruppen2.infrastructure.GroupCache;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@ -37,11 +37,11 @@ import java.util.UUID;
* Es werden übergebene Gruppen bearbeitet und dementsprechend Events erzeugt und gespeichert.
*/
@Log4j2
@TraceMethodCalls
@RequiredArgsConstructor
@Service
public class GroupService {
private final GroupCache groupCache;
private final EventStoreService eventStoreService;
// ################################# GRUPPE ERSTELLEN ########################################
@ -266,7 +266,7 @@ public class GroupService {
private void applyAndSave(Group group, Event event) throws EventException {
event.init(group.version() + 1);
event.apply(group);
event.apply(group, groupCache);
eventStoreService.saveEvent(event);
}

View File

@ -1,215 +0,0 @@
package mops.gruppen2.domain.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.event.Event;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.exception.GroupNotFoundException;
import mops.gruppen2.domain.helper.CommonHelper;
import mops.gruppen2.domain.model.group.Group;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Liefert verschiedene Projektionen auf Gruppen.
* Benötigt ausschließlich den EventStoreService.
*/
@Log4j2
@RequiredArgsConstructor
@Service
public class ProjectionService {
private final EventStoreService eventStoreService;
// ################################## STATISCHE PROJEKTIONEN #################################
/**
* Projiziert Events, geht aber davon aus, dass alle zu derselben Gruppe gehören.
*
* @param events Eventliste
*
* @return Eine projizierte Gruppe
*
* @throws EventException Projektionsfehler, z.B. falls Events von verschiedenen Gruppen übergeben werden
*/
private static Group projectGroupByEvents(List<Event> events) throws EventException {
if (events.isEmpty()) {
throw new GroupNotFoundException(ProjectionService.class.toString());
}
Group group = new Group();
events.forEach(event -> event.apply(group));
return group;
}
/**
* Konstruiert Gruppen aus einer Liste von Events.
*
* @param events Liste an Events
*
* @return Liste an Projizierten Gruppen
*
* @throws EventException Projektionsfehler
*/
public static List<Group> projectGroupsByEvents(List<Event> events) throws EventException {
Map<UUID, Group> groupMap = new HashMap<>();
events.forEach(event -> event.apply(getOrCreateGroup(groupMap, event.getGroupid())));
return new ArrayList<>(groupMap.values());
}
/**
* Gibt die Gruppe mit der richtigen Id aus der übergebenen Map wieder, existiert diese nicht
* wird die Gruppe erstellt und der Map hizugefügt.
*
* @param groups Map aus GruppenIds und Gruppen
* @param groupId Die Id der Gruppe, die zurückgegeben werden soll
*
* @return Die gesuchte Gruppe
*/
private static Group getOrCreateGroup(Map<UUID, Group> groups, UUID groupId) {
if (!groups.containsKey(groupId)) {
groups.put(groupId, new Group());
}
return groups.get(groupId);
}
// ############################### PROJEKTIONEN MIT DATENBANK ################################
/**
* Gibt die Gruppe zurück, die zu der übergebenen Id passt.
* Enthält alle verfügbaren Informationen, also auch User (langsam).
* Gibt eine leere Gruppe zurück, falls die Id leer ist.
*
* @param groupId Die Id der gesuchten Gruppe
*
* @return Die gesuchte Gruppe
*
* @throws GroupNotFoundException Wenn die Gruppe nicht gefunden wird
*/
public Group projectGroupById(UUID groupId) throws GroupNotFoundException {
try {
List<Event> events = eventStoreService.findGroupEvents(groupId);
return projectGroupByEvents(events);
} catch (Exception e) {
log.error("Gruppe {} wurde nicht gefunden!", groupId.toString(), e);
throw new GroupNotFoundException(groupId + ": " + ProjectionService.class);
}
}
public Group projectParent(UUID parent) {
if (CommonHelper.uuidIsEmpty(parent)) {
return new Group();
}
return projectGroupById(parent);
}
public List<Group> projectGroupsByIds(List<UUID> groupids) {
List<Event> events = eventStoreService.findGroupEvents(groupids);
return projectGroupsByEvents(events);
}
/**
* Projiziert Gruppen, welche sich seit einer übergebenen eventId geändert haben.
* Die Gruppen werden dabei vollständig konstruiert.
*
* @param status Letzte bekannte eventId
*
* @return Liste an Gruppen
*/
public List<Group> projectChangedGroups(long status) {
List<UUID> changedids = eventStoreService.findChangedGroups(status);
return projectGroupsByIds(changedids);
}
/**
* Projiziert öffentliche Gruppen.
* Die Gruppen enthalten Metainformationen: Titel, Beschreibung und MaxUserAnzahl.
* Außerdem wird noch beachtet, ob der eingeloggte User bereits in entsprechenden Gruppen mitglied ist.
*
* @return Liste von projizierten Gruppen
*
* @throws EventException Projektionsfehler
*/
@Cacheable("groups")
public List<Group> projectPublicGroups() throws EventException {
List<UUID> groupIds = eventStoreService.findPublicGroupIds();
if (groupIds.isEmpty()) {
return Collections.emptyList();
}
return projectGroupsByIds(groupIds);
}
/**
* Projiziert Vorlesungen.
* Projektionen enthalten nur Metainformationen: Titel.
*
* @return Liste von Veranstaltungen
*/
@Cacheable("groups")
public List<Group> projectLectures() {
List<UUID> groupIds = eventStoreService.findLectureGroupIds();
if (groupIds.isEmpty()) {
return Collections.emptyList();
}
return projectGroupsByIds(groupIds);
}
/**
* Projiziert Gruppen, in welchen der User aktuell teilnimmt.
* Die Gruppen enthalten nur Metainformationen: Titel und Beschreibung.
*
* @param userid Die Id
*
* @return Liste aus Gruppen
*/
@Cacheable("groups")
public List<Group> projectUserGroups(String userid) {
List<UUID> groupIds = eventStoreService.findExistingUserGroups(userid);
if (groupIds.isEmpty()) {
return Collections.emptyList();
}
return projectGroupsByIds(groupIds);
}
/**
* Entfernt alle Gruppen, in welchen ein User teilnimmt, aus einer Gruppenliste.
*
* @param groups Gruppenliste, aus der entfernt wird
* @param userid User, welcher teilnimmt
*/
void removeUserGroups(List<Group> groups, String userid) {
List<UUID> userGroups = eventStoreService.findExistingUserGroups(userid);
groups.removeIf(group -> userGroups.contains(group.getId()));
}
public Group projectGroupByLink(String link) {
return projectGroupById(eventStoreService.findGroupByLink(link));
}
}

View File

@ -1,24 +1,21 @@
package mops.gruppen2.domain.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.SortHelper;
import org.springframework.cache.annotation.Cacheable;
import mops.gruppen2.infrastructure.GroupCache;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Log4j2
public class SearchService {
private final ProjectionService projectionService;
public SearchService(ProjectionService projectionService) {
this.projectionService = projectionService;
}
private final GroupCache groupCache;
/**
* Filtert alle öffentliche Gruppen nach dem Suchbegriff und gibt diese als sortierte Liste zurück.
@ -31,13 +28,11 @@ public class SearchService {
*
* @throws EventException Projektionsfehler
*/
@Cacheable("groups")
//TODO: search in lectures
public List<Group> searchPublicGroups(String search, String principal) {
List<Group> groups = projectionService.projectPublicGroups();
System.out.println(groups);
projectionService.removeUserGroups(groups, principal);
System.out.println(groups);
SortHelper.sortByGroupType(groups);
List<Group> groups = groupCache.publics();
groups = removeUserGroups(groups, principal);
if (search.isEmpty()) {
return groups;
@ -50,4 +45,10 @@ public class SearchService {
.collect(Collectors.toList());
}
private static List<Group> removeUserGroups(List<Group> groups, String principal) {
return groups.stream()
.filter(group -> !group.isMember(principal))
.collect(Collectors.toList());
}
}

View File

@ -1,10 +1,10 @@
package mops.gruppen2.domain.helper;
package mops.gruppen2.domain.service.helper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.web.api.GroupRequestWrapper;
import mops.gruppen2.infrastructure.api.GroupRequestWrapper;
import java.util.List;

View File

@ -1,4 +1,4 @@
package mops.gruppen2.domain.helper;
package mops.gruppen2.domain.service.helper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

View File

@ -1,4 +1,4 @@
package mops.gruppen2.domain.helper;
package mops.gruppen2.domain.service.helper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;

View File

@ -1,4 +1,4 @@
package mops.gruppen2.domain.helper;
package mops.gruppen2.domain.service.helper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

View File

@ -0,0 +1,49 @@
package mops.gruppen2.domain.service.helper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.event.Event;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.infrastructure.GroupCache;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Liefert verschiedene Projektionen auf Gruppen.
* Benötigt ausschließlich den EventStoreService.
*/
@Log4j2
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ProjectionHelper {
public static void project(Map<UUID, Group> groups, List<Event> events, GroupCache cache) {
if (events.isEmpty()) {
return;
}
log.trace(groups);
log.trace(events);
events.forEach(event -> event.apply(getOrCreateGroup(groups, event.getGroupid()), cache));
}
/**
* Gibt die Gruppe mit der richtigen Id aus der übergebenen Map wieder, existiert diese nicht
* wird die Gruppe erstellt und der Map hizugefügt.
*
* @param groups Map aus GruppenIds und Gruppen
* @param groupId Die Id der Gruppe, die zurückgegeben werden soll
*
* @return Die gesuchte Gruppe
*/
private static Group getOrCreateGroup(Map<UUID, Group> groups, UUID groupId) {
if (!groups.containsKey(groupId)) {
groups.put(groupId, new Group());
}
return groups.get(groupId);
}
}

View File

@ -1,4 +1,4 @@
package mops.gruppen2.domain.helper;
package mops.gruppen2.domain.service.helper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

View File

@ -0,0 +1,18 @@
package mops.gruppen2.infrastructure;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@Component
public class ApplicationInit {
private final GroupCache groupCache;
@EventListener(ApplicationReadyEvent.class)
public void init() {
groupCache.init();
}
}

View File

@ -0,0 +1,79 @@
package mops.gruppen2.infrastructure;
import lombok.RequiredArgsConstructor;
import mops.gruppen2.domain.exception.GroupNotFoundException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.domain.service.helper.ProjectionHelper;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Component
@Scope("singleton")
public class GroupCache {
private final EventStoreService eventStoreService;
private final Map<UUID, Group> groups = new HashMap<>();
public void init() {
long maxid = eventStoreService.findMaxEventId();
ProjectionHelper.project(groups, eventStoreService.findNewEvents(0, maxid), this);
}
public void put(Group group) {
groups.put(group.getId(), group);
}
public void remove(Group group) {
groups.remove(group.getId());
}
// Getters
public Group group(UUID groupid) {
if (!groups.containsKey(groupid)) {
throw new GroupNotFoundException("Gruppe ist nicht im Cache.");
}
return groups.get(groupid);
}
public Group group(String link) {
return groups.values().stream()
.filter(group -> group.getLink().equals(link))
.findFirst()
.orElseThrow(() -> new GroupNotFoundException("Link nicht im Cache."));
}
public List<Group> userGroups(String userid) {
return groups.values().stream()
.filter(group -> group.isMember(userid))
.collect(Collectors.toUnmodifiableList());
}
public List<Group> publics() {
return groups.values().stream()
.filter(Group::isPublic)
.collect(Collectors.toUnmodifiableList());
}
public List<Group> privates() {
return groups.values().stream()
.filter(Group::isPrivate)
.collect(Collectors.toUnmodifiableList());
}
public List<Group> lectures() {
return groups.values().stream()
.filter(Group::isLecture)
.collect(Collectors.toUnmodifiableList());
}
}

View File

@ -1,5 +1,6 @@
package mops.gruppen2.web;
package mops.gruppen2.infrastructure;
import lombok.RequiredArgsConstructor;
import mops.gruppen2.domain.Account;
import mops.gruppen2.domain.model.group.Role;
import mops.gruppen2.domain.model.group.Type;
@ -9,9 +10,12 @@ import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
@RequiredArgsConstructor
@ControllerAdvice
public class ModelAttributeControllerAdvice {
private final GroupCache groupCache;
// Add modelAttributes before each @RequestMapping
@ModelAttribute
public void modelAttributes(KeycloakAuthenticationToken token,

View File

@ -1,4 +1,4 @@
package mops.gruppen2.web.api;
package mops.gruppen2.infrastructure.api;
import lombok.AllArgsConstructor;
import lombok.Getter;

View File

@ -1,4 +1,4 @@
package mops.gruppen2.web;
package mops.gruppen2.infrastructure.controller;
import io.swagger.annotations.ApiOperation;
@ -6,12 +6,8 @@ import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.aspect.annotation.TraceMethodCalls;
import mops.gruppen2.domain.helper.APIHelper;
import mops.gruppen2.domain.helper.CommonHelper;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.domain.service.ProjectionService;
import mops.gruppen2.web.api.GroupRequestWrapper;
import mops.gruppen2.domain.service.helper.CommonHelper;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -19,7 +15,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.UUID;
/**
* Api zum Datenabgleich.
@ -31,8 +26,9 @@ import java.util.UUID;
@RequestMapping("/gruppen2/api")
public class APIController {
//TODO: redo api
private final EventStoreService eventStoreService;
private final ProjectionService projectionService;
/**
* Erzeugt eine Liste aus Gruppen, welche sich seit einer übergebenen Event-Id geändert haben.
@ -40,15 +36,15 @@ public class APIController {
*
* @param eventId Die Event-ID, welche der Anfragesteller beim letzten Aufruf erhalten hat
*/
@GetMapping("/update/{id}")
/*@GetMapping("/update/{id}")
@Secured("ROLE_api_user")
@ApiOperation("Gibt veränderte Gruppen zurück")
public GroupRequestWrapper getApiUpdate(@ApiParam("Letzte gespeicherte EventId des Anfragestellers")
@PathVariable("id") long eventId) {
return APIHelper.wrap(eventStoreService.findMaxEventId(),
projectionService.projectChangedGroups(eventId));
}
projectionHelper.projectChangedGroups(eventId));
}*/
/**
* Gibt die Gruppen-IDs von Gruppen, in welchen der übergebene Nutzer teilnimmt, zurück.
@ -65,13 +61,13 @@ public class APIController {
/**
* Konstruiert eine einzelne, vollständige Gruppe.
*/
@GetMapping("/group/{id}")
/*@GetMapping("/group/{id}")
@Secured("ROLE_api_user")
@ApiOperation("Gibt die Gruppe mit der als Parameter mitgegebenden groupId zurück")
public Group getApiGroup(@ApiParam("Gruppen-Id der gefordeten Gruppe")
@PathVariable("id") String groupId) {
return projectionService.projectGroupById(UUID.fromString(groupId));
}
return projectionHelper.projectGroupById(UUID.fromString(groupId));
}*/
}

View File

@ -1,10 +1,8 @@
package mops.gruppen2.web;
package mops.gruppen2.infrastructure.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.aspect.annotation.TraceMethodCalls;
import mops.gruppen2.domain.helper.CsvHelper;
import mops.gruppen2.domain.helper.ValidationHelper;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.domain.model.group.User;
@ -13,9 +11,10 @@ import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.domain.model.group.wrapper.Parent;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.domain.service.GroupService;
import mops.gruppen2.domain.service.ProjectionService;
import mops.gruppen2.domain.service.helper.CsvHelper;
import mops.gruppen2.domain.service.helper.ValidationHelper;
import mops.gruppen2.infrastructure.GroupCache;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@ -35,21 +34,20 @@ import javax.validation.Valid;
@RequestMapping("/gruppen2")
public class GroupCreationController {
private final GroupCache groupCache;
private final GroupService groupService;
private final ProjectionService projectionService;
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping("/create")
public String getCreate(Model model) {
model.addAttribute("lectures", projectionService.projectLectures());
model.addAttribute("lectures", groupCache.lectures());
return "create";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/create")
@CacheEvict(value = "groups", allEntries = true)
public String postCreateOrga(KeycloakAuthenticationToken token,
@RequestParam("type") Type type,
@RequestParam("parent") @Valid Parent parent,

View File

@ -1,19 +1,18 @@
package mops.gruppen2.web;
package mops.gruppen2.infrastructure.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.aspect.annotation.TraceMethodCalls;
import mops.gruppen2.domain.helper.CsvHelper;
import mops.gruppen2.domain.helper.ValidationHelper;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.User;
import mops.gruppen2.domain.model.group.wrapper.Description;
import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.domain.service.GroupService;
import mops.gruppen2.domain.service.ProjectionService;
import mops.gruppen2.domain.service.helper.CsvHelper;
import mops.gruppen2.domain.service.helper.ValidationHelper;
import mops.gruppen2.infrastructure.GroupCache;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@ -36,8 +35,8 @@ import java.util.UUID;
@RequestMapping("/gruppen2")
public class GroupDetailsController {
private final GroupCache groupCache;
private final GroupService groupService;
private final ProjectionService projectionService;
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping("/details/{id}")
@ -46,10 +45,13 @@ public class GroupDetailsController {
@PathVariable("id") String groupId) {
String principal = token.getName();
Group group = projectionService.projectGroupById(UUID.fromString(groupId));
Group group = groupCache.group(UUID.fromString(groupId));
// Parent Badge
Group parent = projectionService.projectParent(group.getParent());
Group parent = Group.EMPTY();
if (group.hasParent()) {
parent = groupCache.group(group.getParent());
}
model.addAttribute("group", group);
model.addAttribute("parent", parent);
@ -64,12 +66,11 @@ public class GroupDetailsController {
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/details/{id}/join")
@CacheEvict(value = "groups", allEntries = true)
public String postDetailsJoin(KeycloakAuthenticationToken token,
@PathVariable("id") String groupId) {
String principal = token.getName();
Group group = projectionService.projectGroupById(UUID.fromString(groupId));
Group group = groupCache.group(UUID.fromString(groupId));
if (ValidationHelper.checkIfMember(group, principal)) {
return "redirect:/gruppen2/details/" + groupId;
@ -82,12 +83,11 @@ public class GroupDetailsController {
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/details/{id}/leave")
@CacheEvict(value = "groups", allEntries = true)
public String postDetailsLeave(KeycloakAuthenticationToken token,
@PathVariable("id") String groupId) {
String principal = token.getName();
Group group = projectionService.projectGroupById(UUID.fromString(groupId));
Group group = groupCache.group(UUID.fromString(groupId));
groupService.kickMember(group, principal, principal);
@ -102,7 +102,7 @@ public class GroupDetailsController {
@PathVariable("id") String groupId) {
String principal = token.getName();
Group group = projectionService.projectGroupById(UUID.fromString(groupId));
Group group = groupCache.group(UUID.fromString(groupId));
// Invite Link
String actualURL = request.getRequestURL().toString();
@ -119,14 +119,15 @@ public class GroupDetailsController {
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/details/{id}/edit/meta")
@CacheEvict(value = "groups", allEntries = true)
public String postDetailsEditMeta(KeycloakAuthenticationToken token,
@PathVariable("id") String groupId,
@Valid Title title,
@Valid Description description) {
String principal = token.getName();
Group group = projectionService.projectGroupById(UUID.fromString(groupId));
Group group = groupCache.group(UUID.fromString(groupId));
System.out.println(group);
groupService.setTitle(group, principal, title);
groupService.setDescription(group, principal, description);
@ -136,12 +137,11 @@ public class GroupDetailsController {
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/details/{id}/edit/userlimit")
@CacheEvict(value = "groups", allEntries = true)
public String postDetailsEditUserLimit(KeycloakAuthenticationToken token,
@PathVariable("id") String groupId,
@Valid Limit limit) {
String principal = token.getName();
Group group = projectionService.projectGroupById(UUID.fromString(groupId));
Group group = groupCache.group(UUID.fromString(groupId));
groupService.setLimit(group, principal, limit);
@ -150,13 +150,12 @@ public class GroupDetailsController {
@RolesAllowed("ROLE_orga")
@PostMapping("/details/{id}/edit/csv")
@CacheEvict(value = "groups", allEntries = true)
public String postDetailsEditCsv(KeycloakAuthenticationToken token,
@PathVariable("id") String groupId,
@RequestParam(value = "file", required = false) MultipartFile file) {
String principal = token.getName();
Group group = projectionService.projectGroupById(UUID.fromString(groupId));
Group group = groupCache.group(UUID.fromString(groupId));
groupService.addUsersToGroup(group, principal, CsvHelper.readCsvFile(file));
@ -165,13 +164,12 @@ public class GroupDetailsController {
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/details/{id}/edit/role/{userid}")
@CacheEvict(value = "groups", allEntries = true)
public String postDetailsEditRole(KeycloakAuthenticationToken token,
@PathVariable("id") String groupId,
@PathVariable("userid") String target) {
String principal = token.getName();
Group group = projectionService.projectGroupById(UUID.fromString(groupId));
Group group = groupCache.group(UUID.fromString(groupId));
ValidationHelper.throwIfNoAdmin(group, principal);
@ -187,13 +185,12 @@ public class GroupDetailsController {
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/details/{id}/edit/delete/{userid}")
@CacheEvict(value = "groups", allEntries = true)
public String postDetailsEditDelete(KeycloakAuthenticationToken token,
@PathVariable("id") String groupId,
@PathVariable("userid") String target) {
String principal = token.getName();
Group group = projectionService.projectGroupById(UUID.fromString(groupId));
Group group = groupCache.group(UUID.fromString(groupId));
ValidationHelper.throwIfNoAdmin(group, principal);
@ -207,12 +204,11 @@ public class GroupDetailsController {
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/details/{id}/edit/destroy")
@CacheEvict(value = "groups", allEntries = true)
public String postDetailsEditDestroy(KeycloakAuthenticationToken token,
@PathVariable("id") String groupid) {
String principal = token.getName();
Group group = projectionService.projectGroupById(UUID.fromString(groupid));
Group group = groupCache.group(UUID.fromString(groupid));
groupService.deleteGroup(group, principal);

View File

@ -1,10 +1,10 @@
package mops.gruppen2.web;
package mops.gruppen2.infrastructure.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.aspect.annotation.TraceMethodCall;
import mops.gruppen2.domain.exception.PageNotFoundException;
import mops.gruppen2.domain.service.ProjectionService;
import mops.gruppen2.infrastructure.GroupCache;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
@ -20,22 +20,25 @@ import javax.servlet.http.HttpServletRequest;
@Controller
public class GruppenfindungController {
private final ProjectionService projectionService;
private final GroupCache groupCache;
// For convenience
//@GetMapping("")
//public String redirect() {
// return "redirect:/gruppen2";
//}
@GetMapping("")
public String redirect() {
return "redirect:/gruppen2";
}
@GetMapping("/login")
public String login() {
return "redirect:/gruppen2";
}
@TraceMethodCall
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping("/gruppen2")
public String getIndexPage(KeycloakAuthenticationToken token,
Model model) {
String principal = token.getName();
model.addAttribute("groups", projectionService.projectUserGroups(principal));
model.addAttribute("groups", groupCache.userGroups(token.getName()));
return "index";
}

View File

@ -1,13 +1,13 @@
package mops.gruppen2.web;
package mops.gruppen2.infrastructure.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.aspect.annotation.TraceMethodCalls;
import mops.gruppen2.domain.helper.ValidationHelper;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.domain.service.ProjectionService;
import mops.gruppen2.domain.service.SearchService;
import mops.gruppen2.domain.service.helper.ValidationHelper;
import mops.gruppen2.infrastructure.GroupCache;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
@ -29,7 +29,7 @@ import java.util.List;
@RequestMapping("/gruppen2")
public class SearchAndInviteController {
private final ProjectionService projectionService;
private final GroupCache groupCache;
private final SearchService searchService;
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@ -62,7 +62,7 @@ public class SearchAndInviteController {
@PathVariable("link") String link) {
String principal = token.getName();
Group group = projectionService.projectGroupByLink(link);
Group group = groupCache.group(link);
model.addAttribute("group", group);

View File

@ -11,43 +11,21 @@ import java.util.List;
@Repository
public interface EventRepository extends CrudRepository<EventDTO, Long> {
// ####################################### GROUP IDs #########################################
/*@Query("SELECT DISTINCT group_id FROM event"
+ " WHERE user_id = :userId AND event_type = :type")
List<String> findGroupIdsByUserAndType(@Param("userId") String userId,
@Param("type") String type);*/
@Query("SELECT DISTINCT group_id FROM event"
+ " WHERE event_id > :status")
List<String> findGroupIdsWhereEventIdGreaterThanStatus(@Param("status") long status);
// ####################################### EVENT DTOs ########################################
@Query("SELECT * FROM event"
+ " WHERE group_id IN (:groupIds) ")
List<EventDTO> findEventDTOsByGroup(@Param("groupIds") List<String> groupIds);
/*@Query("SELECT * FROM event"
+ " WHERE group_id IN (:userIds) ")
List<EventDTO> findEventDTOsByUser(@Param("groupIds") String... userIds);*/
@Query("SELECT * FROM event WHERE event_id > :version AND event_id <= :max")
List<EventDTO> findNewEvents(@Param("version") long version,
@Param("max") long maxid);
@Query("SELECT * FROM event"
+ " WHERE event_type IN (:types)")
List<EventDTO> findEventDTOsByType(@Param("types") List<String> types);
@Query("SELECT * FROM event")
List<EventDTO> findAllEvents();
@Query("SELECT * FROM event"
+ " WHERE event_type IN (:types) AND group_id IN (:groupIds)")
List<EventDTO> findEventDTOsByGroupAndType(@Param("groupIds") List<String> groupIds,
@Param("types") List<String> types);
/*@Query("SELECT * FROM event"
+ " WHERE event_type IN (:types) AND user_id = :userId")
List<EventDTO> findEventDTOsByUserAndType(@Param("userId") String userId,
@Param("types") String... types);*/
// ################################ LATEST EVENT DTOs ########################################
@Query("WITH ranked_events AS ("
+ "SELECT *, ROW_NUMBER() OVER (PARTITION BY group_id ORDER BY event_id DESC) AS rn"
+ " FROM event"

View File

@ -12,11 +12,6 @@ spring.datasource.password =
spring.jpa.database-platform = org.hibernate.dialect.H2Dialect
spring.h2.console.enabled = false
# Security
keycloak.auth-server-url = http://localhost:8082/auth
hhu_keycloak.token-uri = http://localhost:8082/auth/realms/Gruppen/protocol/openid-connect/token
# Misc
server.error.include-stacktrace = always
management.endpoints.web.exposure.include = info,health

View File

@ -10,11 +10,6 @@ spring.datasource.url = jdbc:mysql://dbmysql:3306/gruppen
spring.datasource.username = gruppen
spring.datasource.password = password
# Security
keycloak.auth-server-url = http://localhost:8082/auth
hhu_keycloak.token-uri = http://localhost:8082/auth/realms/Gruppen/protocol/openid-connect/token
# Misc
management.endpoints.web.exposure.include = info,health
server.error.include-stacktrace = always

View File

@ -10,10 +10,6 @@ spring.datasource.url = mysql://b4b665d39d0670:cc933ff7@eu
spring.datasource.username = b4b665d39d0670
spring.datasource.password = cc933ff7
# Security
keycloak.auth-server-url = https://gruppenkeycloak.herokuapp.com/auth
hhu_keycloak.token-uri = https://gruppenkeycloak.herokuapp.com/auth/realms/master/protocol/openid-connect/token
# Misc
management.endpoints.web.exposure.include = info,health

View File

@ -11,6 +11,8 @@ spring.profiles.active = dev
#keycloak.use-resource-role-mappings = true
#keycloak.autodetect-bearer-only = true
#keycloak.confidential-port = 443
keycloak.auth-server-url = https://gruppenkeycloak.herokuapp.com/auth
hhu_keycloak.token-uri = https://gruppenkeycloak.herokuapp.com/auth/realms/master/protocol/openid-connect/token
keycloak.principal-attribute = preferred_username
keycloak.realm = master
keycloak.resource = gruppen-app

View File

@ -30,7 +30,7 @@ class ControllerTest {
public static final ArchRule controllerClassesShouldBeInControllerPackage = classes()
.that().areAnnotatedWith(Controller.class)
.or().areAnnotatedWith(RestController.class)
.should().resideInAPackage("..web");
.should().resideInAPackage("..controller");
@ArchTest
public static final ArchRule classesInControllerPackageShouldHaveControllerInName = classes()

View File

@ -25,11 +25,11 @@ class ServiceTest {
@ArchTest
public static final ArchRule serviceClassesShouldBeInServicePackage = classes()
.that().areAnnotatedWith(Service.class)
.should().resideInAPackage("..service..");
.should().resideInAPackage("..service");
@ArchTest
public static final ArchRule classesInServicePackageShouldHaveServiceInName = classes()
.that().resideInAPackage("..service..")
.that().resideInAPackage("..service")
.should().haveSimpleNameEndingWith("Service");
@ArchIgnore