From dc4cdcc44a76a4e259ceb03674ff82a55ffcb020 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Tue, 7 Oct 2025 22:08:52 +0200 Subject: [PATCH 1/3] Logging and comments improved. --- .../internal/AbstractInternalListener.java | 8 ++++++++ .../DefaultEventPublicationService.java | 8 ++++++++ .../keycloak/DefaultKeycloakClient.java | 2 +- .../keycloak/DefaultUserConfiguration.java | 2 +- .../persistence/mapper/LeaderboardMapper.java | 2 ++ .../persistence/mapper/UserMapper.java | 2 ++ .../persistence/mapper/UserScoreMapper.java | 1 + .../service/DefaultCourseService.java | 7 +++++++ .../service/DefaultLeaderboardService.java | 18 ++++++++++++++++++ .../service/DefaultUserService.java | 15 +++++++++++++++ .../service/ILeaderboardService.java | 17 +++++++++++++++++ .../service/IUserService.java | 14 ++++++++++++++ .../functional/DefaultScoringFunction.java | 7 +++++++ .../functional/DefaultXPImplementation.java | 13 +++++++++++++ .../service/internal/DefaultUserXPAdder.java | 19 +++++++++++++++++++ .../service/internal/ICourseCreator.java | 6 ++++++ .../service/internal/IUserCreator.java | 1 + .../service/internal/IUserXPAdder.java | 3 +++ .../xp/AchievementCompletedEventListener.java | 6 ++++++ .../xp/ChapterCompletionXPListener.java | 1 + .../xp/ContentProgressedXPListener.java | 1 + .../xp/CourseCompletionXPListener.java | 1 + .../xp/ForumActivityEventXPListener.java | 1 + .../xp/MediaRecordWorkedOnXPListener.java | 1 + .../reactive/xp/StageCompletedXPListener.java | 1 + .../xp/UserProgressUpdatedXPListener.java | 3 ++- .../time/DefaultPeriodCalculator.java | 13 +++++++++++++ .../time/DefaultTimeService.java | 19 +++++++++++++++++++ 28 files changed, 189 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/AbstractInternalListener.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/AbstractInternalListener.java index 44da5113..fdfcf44b 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/AbstractInternalListener.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/AbstractInternalListener.java @@ -1,5 +1,6 @@ package de.unistuttgart.iste.meitrex.gamification_service.events.internal; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentEvent; import de.unistuttgart.iste.meitrex.gamification_service.events.repository.IPersistentEventRepository; import de.unistuttgart.iste.meitrex.gamification_service.events.repository.IPersistentEventStatusRepository; @@ -43,6 +44,13 @@ public final UUID getListenerUUID() { return UUID.nameUUIDFromBytes(getName().getBytes(StandardCharsets.UTF_8)); } + @Loggable( + inLogLevel = Loggable.LogLevel.INFO, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false, + logExit = false + ) public void process(V internalEvent) { assureIsValid(internalEvent); process(fetchPersistentEvent(internalEvent)); diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/publication/DefaultEventPublicationService.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/publication/DefaultEventPublicationService.java index e1f83ade..9f44b699 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/publication/DefaultEventPublicationService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/publication/DefaultEventPublicationService.java @@ -1,5 +1,6 @@ package de.unistuttgart.iste.meitrex.gamification_service.events.publication; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import de.unistuttgart.iste.meitrex.gamification_service.events.PersistentMediaRecordWorkedOnEvent; import de.unistuttgart.iste.meitrex.gamification_service.events.internal.*; import de.unistuttgart.iste.meitrex.gamification_service.events.persistent.*; @@ -109,6 +110,13 @@ public DefaultEventPublicationService( } @Override + @Loggable( + inLogLevel = Loggable.LogLevel.DEBUG, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logArgs = false, + logExecutionTime = false + ) public void saveCommitAndPublishIfNew(PersistentEvent persistentEvent) { if(!this.handlerMap.containsKey(persistentEvent.getClass())) { throw new IllegalArgumentException(ERR_MSG_UNSUPPORTED_EVENT_TYPE); diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/keycloak/DefaultKeycloakClient.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/keycloak/DefaultKeycloakClient.java index 4192bb24..990f1d8b 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/keycloak/DefaultKeycloakClient.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/keycloak/DefaultKeycloakClient.java @@ -60,7 +60,7 @@ public DefaultKeycloakClient( @Override @Loggable( - inLogLevel = Loggable.LogLevel.INFO, + inLogLevel = Loggable.LogLevel.DEBUG, exitLogLevel = Loggable.LogLevel.DEBUG, exceptionLogLevel = Loggable.LogLevel.WARN, logExecutionTime = false diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/keycloak/DefaultUserConfiguration.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/keycloak/DefaultUserConfiguration.java index 72ad2e12..f94083d6 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/keycloak/DefaultUserConfiguration.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/keycloak/DefaultUserConfiguration.java @@ -38,7 +38,7 @@ public DefaultUserConfiguration(IKeycloakClient keycloakClient) { @Override @Loggable( - inLogLevel = Loggable.LogLevel.INFO, + inLogLevel = Loggable.LogLevel.DEBUG, exitLogLevel = Loggable.LogLevel.DEBUG, exceptionLogLevel = Loggable.LogLevel.WARN, logExecutionTime = false diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/persistence/mapper/LeaderboardMapper.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/persistence/mapper/LeaderboardMapper.java index 51fdb613..a654e1df 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/persistence/mapper/LeaderboardMapper.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/persistence/mapper/LeaderboardMapper.java @@ -3,6 +3,7 @@ import java.util.*; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -16,6 +17,7 @@ public class LeaderboardMapper { private final UserScoreMapper userScoreMapper; + public Leaderboard toDTO(LeaderboardEntity leaderboardEntity, int maxDepth) { if(leaderboardEntity == null || maxDepth < 0) { return null; diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/persistence/mapper/UserMapper.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/persistence/mapper/UserMapper.java index 784ce4f3..929007f6 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/persistence/mapper/UserMapper.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/persistence/mapper/UserMapper.java @@ -1,6 +1,7 @@ package de.unistuttgart.iste.meitrex.gamification_service.persistence.mapper; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; @@ -14,6 +15,7 @@ @RequiredArgsConstructor public class UserMapper { + public User toDTO(UserEntity userEntity, int maxDepth) { if(userEntity == null || maxDepth < 0) { return null; diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/persistence/mapper/UserScoreMapper.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/persistence/mapper/UserScoreMapper.java index 4101610d..58930892 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/persistence/mapper/UserScoreMapper.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/persistence/mapper/UserScoreMapper.java @@ -1,5 +1,6 @@ package de.unistuttgart.iste.meitrex.gamification_service.persistence.mapper; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/DefaultCourseService.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/DefaultCourseService.java index 1042b2ee..bca6f23d 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/DefaultCourseService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/DefaultCourseService.java @@ -3,6 +3,7 @@ import java.util.*; import de.unistuttgart.iste.meitrex.course_service.client.CourseServiceClient; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import de.unistuttgart.iste.meitrex.gamification_service.service.internal.achievements.ICourseAchievementMapper; import de.unistuttgart.iste.meitrex.gamification_service.service.internal.ICourseCreator; import de.unistuttgart.iste.meitrex.gamification_service.time.IPeriodCalculator; @@ -67,6 +68,12 @@ public DefaultCourseService( } @Override + @Loggable( + inLogLevel = Loggable.LogLevel.INFO, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false + ) public CourseEntity fetchOrCreate(UUID courseId) { return this.courseRepository .findById(courseId) diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/DefaultLeaderboardService.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/DefaultLeaderboardService.java index 1bfddd71..0f723f64 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/DefaultLeaderboardService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/DefaultLeaderboardService.java @@ -3,6 +3,7 @@ import java.time.*; import java.util.*; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import jakarta.transaction.*; import org.springframework.stereotype.*; @@ -100,6 +101,12 @@ public DefaultLeaderboardService( // Interface implementation @Override + @Loggable( + inLogLevel = Loggable.LogLevel.INFO, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false + ) public List find(UUID courseID, LocalDate date, Period period) { return this.leaderboardRepository.findByCourseIdAndDateAfterAndPeriod(courseID, date, period) .stream() @@ -112,12 +119,22 @@ public List find(UUID courseID, LocalDate date, Period period) { @Transactional @Scheduled(cron = "0 0 0 * * MON") + @Loggable( + inLogLevel = Loggable.LogLevel.INFO, + exitLogLevel = Loggable.LogLevel.INFO, + exceptionLogLevel = Loggable.LogLevel.WARN + ) public void runWeeklyLeaderboardUpdate() { this.updateAllCourseLeaderboards(Period.WEEKLY); } @Transactional @Scheduled(cron = "0 0 0 1 * *") + @Loggable( + inLogLevel = Loggable.LogLevel.INFO, + exitLogLevel = Loggable.LogLevel.INFO, + exceptionLogLevel = Loggable.LogLevel.WARN + ) public void runMonthlyLeaderboardUpdate() { this.updateAllCourseLeaderboards(Period.MONTHLY); } @@ -135,6 +152,7 @@ public List findAllWeekly() { @Autowired private LeaderboardMapper mapper; + public List findAll(Period period) { List list = new ArrayList<>(); this.courseRepository diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/DefaultUserService.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/DefaultUserService.java index d0349652..0c523037 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/DefaultUserService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/DefaultUserService.java @@ -3,6 +3,7 @@ import java.util.*; import de.unistuttgart.iste.meitrex.course_service.client.CourseServiceClient; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import de.unistuttgart.iste.meitrex.gamification_service.exception.ConflictException; import de.unistuttgart.iste.meitrex.gamification_service.exception.ResourceNotFoundException; import de.unistuttgart.iste.meitrex.gamification_service.persistence.entity.items.UserInventoryEntity; @@ -65,6 +66,13 @@ public DefaultUserService( @Override + @Loggable( + inLogLevel = Loggable.LogLevel.INFO, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false, + logResult = false + ) public UserEntity fetchOrCreate(UUID userId) { final UserEntity user = this.userRepository .findById(userId) @@ -73,6 +81,13 @@ public UserEntity fetchOrCreate(UUID userId) { } @Override + @Loggable( + inLogLevel = Loggable.LogLevel.INFO, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false, + logResult = false + ) public User fetchUser(UUID userID) throws ResourceNotFoundException { final UserEntity user = this.fetchOrThrow(userID); diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/ILeaderboardService.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/ILeaderboardService.java index c3c087f6..ba615c76 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/ILeaderboardService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/ILeaderboardService.java @@ -7,8 +7,25 @@ import de.unistuttgart.iste.meitrex.gamification_service.time.Period; +/** + * A service layer interface for fetching information reflecting a users performance as visualized by a leaderboard. The + * results are meant for external exposure, .g. via a GraphQL API, and should not be consumed by business logic. + * + * @author Philipp Kunz + */ public interface ILeaderboardService { + /** + * Retrieves leaderboard data representing the relative performance of users participating in the course identified by + * {@code courseID}, within the time range defined by the given {@code date} and {@code period}. + * If no matching leaderboard data is found, an empty list is returned. + * + * @param courseID the uuid of the course for which leaderboard data should be retrieved. + * @param date the start of the period of interest. + * @param period the period that, together with {@code date}, determines the duration of the leaderboard + * (e.g. daily, weekly, monthly). + * @return a list of leaderboards. + */ List find(UUID courseID, LocalDate date, Period period); } diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/IUserService.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/IUserService.java index 3850ceb3..dab96a53 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/IUserService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/IUserService.java @@ -5,8 +5,22 @@ import de.unistuttgart.iste.meitrex.generated.dto.*; import de.unistuttgart.iste.meitrex.gamification_service.exception.*; +/** + * A service layer interface for fetching gamification-related user data. The results are meant for external exposure, + * e.g. via a GraphQL API, and should not be consumed by business logic. + * + * @author Philipp Kunz + */ public interface IUserService { + /** + * Fetches for the passed {@code userID} the corresponding instance of {@code User}. If no such entity exists + * a {@code ResourceNotFoundException} is thrown. + * + * @param userID an uuid identifying a user. + * @return an instance of {@code User} matching the passed user id. + * @throws ResourceNotFoundException if no matching user entity exists. + */ User fetchUser(UUID userID) throws ResourceNotFoundException; } diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/functional/DefaultScoringFunction.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/functional/DefaultScoringFunction.java index 16772014..81eeff5c 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/functional/DefaultScoringFunction.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/functional/DefaultScoringFunction.java @@ -1,5 +1,6 @@ package de.unistuttgart.iste.meitrex.gamification_service.service.functional; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import org.springframework.stereotype.*; import org.springframework.beans.factory.annotation.*; @@ -45,6 +46,12 @@ public DefaultScoringFunction( } @Override + @Loggable( + inLogLevel = Loggable.LogLevel.DEBUG, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false + ) public double score(double correctness, int attempt) { return Math.round(this.multiplicativeWeight*correctness*(Math.pow(2.0, this.decayRate*attempt))); } diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/functional/DefaultXPImplementation.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/functional/DefaultXPImplementation.java index cd1c9fe9..965c2d52 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/functional/DefaultXPImplementation.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/functional/DefaultXPImplementation.java @@ -2,6 +2,7 @@ import java.util.*; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import org.springframework.stereotype.*; import org.springframework.beans.factory.annotation.*; @@ -84,6 +85,12 @@ public DefaultXPImplementation( } @Override + @Loggable( + inLogLevel = Loggable.LogLevel.DEBUG, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false + ) public int calcLevel(double xpValue) { assureNonNegative(xpValue, ERR_MSG_XP_VALUE_MUST_BE_NON_NEGATIVE); for(Map.Entry entry : this.levelXPThresholdMappingList) { @@ -95,6 +102,12 @@ public int calcLevel(double xpValue) { } @Override + @Loggable( + inLogLevel = Loggable.LogLevel.TRACE, + exitLogLevel = Loggable.LogLevel.TRACE, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false + ) public double calcDistance(double curXP, int targetLevel) { assureIsSupportedLevel(this.maxLevel, targetLevel, ERR_MSG_UNSUPPORTED_LEVEL); return this.levelXPThresholdMapping.get(targetLevel) - curXP; diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/DefaultUserXPAdder.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/DefaultUserXPAdder.java index 0180ef30..bb0c742c 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/DefaultUserXPAdder.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/DefaultUserXPAdder.java @@ -1,5 +1,6 @@ package de.unistuttgart.iste.meitrex.gamification_service.service.internal; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import de.unistuttgart.iste.meitrex.gamification_service.persistence.entity.UserEntity; import de.unistuttgart.iste.meitrex.generated.dto.UserItem; import org.springframework.stereotype.Component; @@ -36,6 +37,12 @@ public DefaultUserXPAdder() { } @Override + @Loggable( + inLogLevel = Loggable.LogLevel.DEBUG, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false + ) public void add(UserEntity entity, int value) { Objects.requireNonNull(entity); assureNonNull(value); @@ -48,11 +55,23 @@ public void add(UserEntity entity, int value) { } @Override + @Loggable( + inLogLevel = Loggable.LogLevel.DEBUG, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false + ) public void add(UserEntity entity, Cause cause) { this.add(entity, causeXPMap.get(Objects.requireNonNull(cause))); } @Override + @Loggable( + inLogLevel = Loggable.LogLevel.DEBUG, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false + ) public void add(UserEntity entity, Cause cause, int multiple) { assureNonNull(multiple); this.add(entity, causeXPMap.get(Objects.requireNonNull(cause)) * multiple); diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/ICourseCreator.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/ICourseCreator.java index afab7010..1e50485c 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/ICourseCreator.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/ICourseCreator.java @@ -4,6 +4,12 @@ import java.util.UUID; +/** + * A contract for retrieving locally stored data on a course. Retrieved data is meant for further internal processing + * by business logic. + * + * @author Philipp Kunz + */ public interface ICourseCreator { /** diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/IUserCreator.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/IUserCreator.java index cc62a117..6ad03df0 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/IUserCreator.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/IUserCreator.java @@ -8,6 +8,7 @@ * A contract for fetching a user by its {@see UUID}. If no such exists, it is created and initialized. The * concrete initialization method must be treated as an implementation detail. * + * @author Philipp Kunz */ public interface IUserCreator { diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/IUserXPAdder.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/IUserXPAdder.java index 00f0cfad..06d4ddef 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/IUserXPAdder.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/internal/IUserXPAdder.java @@ -10,6 +10,9 @@ */ public interface IUserXPAdder { + /** + * Represents the type of event for which a user can potentially be awarded additional XP. + */ enum Cause { NEW_FORUM_POST, ANSWER_ACCEPTED, ACHIEVEMENT_COMPLETED, STAGE_COMPLETED, CHAPTER_COMPLETED, COURSE_COMPLETED, ASSIGNMENT_COMPLETED, QUIZ_COMPLETED, FLASHCARD_COMPLETED, VIDEO_WATCHED, DOCUMENT_OPENED, SUBMISSION_COMPLETED } diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/AchievementCompletedEventListener.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/AchievementCompletedEventListener.java index 279bdb9d..5130f80b 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/AchievementCompletedEventListener.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/AchievementCompletedEventListener.java @@ -3,6 +3,7 @@ import java.util.Objects; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import org.springframework.stereotype.*; import org.springframework.context.event.*; import org.springframework.beans.factory.annotation.*; @@ -20,6 +21,11 @@ public AchievementCompletedEventListener(@Autowired IUserXPAdder userXPAdder) { } @EventListener + @Loggable( + inLogLevel = Loggable.LogLevel.INFO, + exitLogLevel = Loggable.LogLevel.INFO, + exceptionLogLevel = Loggable.LogLevel.WARN + ) public void onAchievementCompleted(AchievementCompletedEvent event) { userXPAdder.add(event.getAchiever(), IUserXPAdder.Cause.ACHIEVEMENT_COMPLETED); } diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/ChapterCompletionXPListener.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/ChapterCompletionXPListener.java index 4cfd645e..578fb895 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/ChapterCompletionXPListener.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/ChapterCompletionXPListener.java @@ -1,5 +1,6 @@ package de.unistuttgart.iste.meitrex.gamification_service.service.reactive.xp; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import de.unistuttgart.iste.meitrex.gamification_service.events.internal.*; import de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentChapterCompletedEvent; import de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentCourseCompletedEvent; diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/ContentProgressedXPListener.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/ContentProgressedXPListener.java index cd3b3956..f1c66bed 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/ContentProgressedXPListener.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/ContentProgressedXPListener.java @@ -1,6 +1,7 @@ package de.unistuttgart.iste.meitrex.gamification_service.service.reactive.xp; import de.unistuttgart.iste.meitrex.common.event.ContentProgressedEvent; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import de.unistuttgart.iste.meitrex.gamification_service.events.internal.*; import de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentContentProgressedEvent; import de.unistuttgart.iste.meitrex.gamification_service.events.repository.IPersistentContentProgressedRepository; diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/CourseCompletionXPListener.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/CourseCompletionXPListener.java index 524f79f1..8caa26bc 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/CourseCompletionXPListener.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/CourseCompletionXPListener.java @@ -1,5 +1,6 @@ package de.unistuttgart.iste.meitrex.gamification_service.service.reactive.xp; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import de.unistuttgart.iste.meitrex.gamification_service.events.internal.*; import de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentCourseCompletedEvent; import de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentStageCompletedEvent; diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/ForumActivityEventXPListener.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/ForumActivityEventXPListener.java index d2adcde5..7f53ac23 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/ForumActivityEventXPListener.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/ForumActivityEventXPListener.java @@ -1,6 +1,7 @@ package de.unistuttgart.iste.meitrex.gamification_service.service.reactive.xp; import de.unistuttgart.iste.meitrex.common.event.ForumActivityEvent; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import de.unistuttgart.iste.meitrex.gamification_service.events.internal.AbstractInternalListener; import de.unistuttgart.iste.meitrex.gamification_service.events.internal.InternalForumActivityEvent; import de.unistuttgart.iste.meitrex.gamification_service.events.internal.NonTransientEventListenerException; diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/MediaRecordWorkedOnXPListener.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/MediaRecordWorkedOnXPListener.java index 03a81d4e..7b5eccb0 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/MediaRecordWorkedOnXPListener.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/MediaRecordWorkedOnXPListener.java @@ -1,6 +1,7 @@ package de.unistuttgart.iste.meitrex.gamification_service.service.reactive.xp; import de.unistuttgart.iste.meitrex.common.event.MediaType; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import de.unistuttgart.iste.meitrex.gamification_service.events.PersistentMediaRecordWorkedOnEvent; import de.unistuttgart.iste.meitrex.gamification_service.events.internal.*; import de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentMediaRecordInfoEvent; diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/StageCompletedXPListener.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/StageCompletedXPListener.java index 6f0d6d56..d8365abe 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/StageCompletedXPListener.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/StageCompletedXPListener.java @@ -1,5 +1,6 @@ package de.unistuttgart.iste.meitrex.gamification_service.service.reactive.xp; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import de.unistuttgart.iste.meitrex.gamification_service.events.internal.*; import de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentStageCompletedEvent; import de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentUserProgressUpdatedEvent; diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/UserProgressUpdatedXPListener.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/UserProgressUpdatedXPListener.java index 751f3913..cb4882cd 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/UserProgressUpdatedXPListener.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/service/reactive/xp/UserProgressUpdatedXPListener.java @@ -1,5 +1,6 @@ package de.unistuttgart.iste.meitrex.gamification_service.service.reactive.xp; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import de.unistuttgart.iste.meitrex.gamification_service.persistence.entity.UserEntity; import de.unistuttgart.iste.meitrex.gamification_service.service.internal.IUserCreator; import org.springframework.stereotype.*; @@ -47,7 +48,7 @@ protected String getName() { } @Override - protected void doProcess(PersistentUserProgressUpdatedEvent persistentEvent) + public void doProcess(PersistentUserProgressUpdatedEvent persistentEvent) throws TransientEventListenerException, NonTransientEventListenerException { final Optional additionalXPOptional = this.mapToAdditionalXP(persistentEvent); if(additionalXPOptional.isPresent()) { diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/time/DefaultPeriodCalculator.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/time/DefaultPeriodCalculator.java index 397f861b..f53027ce 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/time/DefaultPeriodCalculator.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/time/DefaultPeriodCalculator.java @@ -3,6 +3,7 @@ import java.time.*; import java.util.*; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import org.springframework.stereotype.*; import org.springframework.beans.factory.annotation.*; @@ -64,6 +65,12 @@ public DefaultPeriodCalculator(@Autowired ITimeService timeService) { } @Override + @Loggable( + inLogLevel = Loggable.LogLevel.DEBUG, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false + ) public LocalDate calcStartDate(long timestamp, Period period) { assureIsNonNull(period); assureIsNonNegative(timestamp); @@ -75,6 +82,12 @@ public LocalDate calcStartDate(long timestamp, Period period) { } @Override + @Loggable( + inLogLevel = Loggable.LogLevel.DEBUG, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false + ) public LocalDate calcSucStartDate(LocalDate startDate, Period period) { assureIsValidBeginDate(startDate, period); LocalDate endDate = switch (period) { diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/time/DefaultTimeService.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/time/DefaultTimeService.java index b47d0e91..d68b00fc 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/time/DefaultTimeService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/time/DefaultTimeService.java @@ -2,17 +2,30 @@ import java.time.*; +import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable; import org.springframework.stereotype.*; @Component public class DefaultTimeService implements ITimeService { @Override + @Loggable( + inLogLevel = Loggable.LogLevel.DEBUG, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false + ) public long curTime() { return System.currentTimeMillis(); } @Override + @Loggable( + inLogLevel = Loggable.LogLevel.DEBUG, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false + ) public LocalDate toDate() { return Instant.ofEpochMilli(this.curTime()) .atZone(ZoneId.systemDefault()) @@ -20,6 +33,12 @@ public LocalDate toDate() { } @Override + @Loggable( + inLogLevel = Loggable.LogLevel.DEBUG, + exitLogLevel = Loggable.LogLevel.DEBUG, + exceptionLogLevel = Loggable.LogLevel.WARN, + logExecutionTime = false + ) public LocalDate toDate(Long time) { return Instant.ofEpochMilli(time) .atZone(ZoneId.systemDefault()) From 12c1cd124101cb6f7e409e460643bc3d7a6a3745 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Wed, 8 Oct 2025 13:13:14 +0200 Subject: [PATCH 2/3] Documentation complemented. --- .../internal/AbstractInternalListener.java | 18 ++++++++++++++++ .../events/internal/InternalEvent.java | 9 ++++++++ .../NonTransientEventListenerException.java | 6 ++++++ .../TransientEventListenerException.java | 6 ++++++ .../events/persistent/PersistentEvent.java | 13 ++++++++++++ .../DefaultEventPublicationService.java | 21 +++++++++++++++++++ .../publication/IEventPublicationService.java | 15 +++++++++++++ 7 files changed, 88 insertions(+) diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/AbstractInternalListener.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/AbstractInternalListener.java index fdfcf44b..b3fd445f 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/AbstractInternalListener.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/AbstractInternalListener.java @@ -11,6 +11,24 @@ import java.nio.charset.StandardCharsets; import java.util.*; +/** + * Abstract base class for internal event listeners in the gamification service. This listener implements generic + * functionality for processing {@link PersistentEvent} and their corresponding {@link InternalEvent}s It handles: + * (1) fetching the associated persistent event from the repository, (2) tracking and updating processing status + * for retries and failures, (3) delegating the actual business logic to subclasses via {@link #doProcess(PersistentEvent)}, + * (4) retry logic with configurable max attempt count. Any concrete listener is expected to be executed within a + * transactional context (e.g., managed by Spring), otherwise, event status updates will not be persisted. + * + * To implement a custom listener for a specific type of event, create a subclass overriding the following methods: + * (1) {@link #getName()} - must return a unique and stable name. + * (2) {@link #doProcess(U persistentEvent))} - contains the actual processing logic. In case of an unrecoverable error, + * an implementation should throw a {@link NonTransientEventListenerException}. Otherwise, {@link TransientEventListenerException} + * should be thrown. In the latter case, the base class logic takes care of updating the event status and retrying. + * + * @param the type of the persistent event associated with the internal event. + * @param the type of the internal event to be processed. + * @author Philiipp Kunz + */ public abstract class AbstractInternalListener { diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/InternalEvent.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/InternalEvent.java index 4ced4107..c1cb0f27 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/InternalEvent.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/InternalEvent.java @@ -8,6 +8,15 @@ import org.springframework.context.*; +/** + * Each {@code InternalEvent} corresponds to a {@link de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentEvent} + * stored in the database and is represented as a Spring {@link org.springframework.context.ApplicationEvent}. These events + * can be published within the application context and are typically consumed by {@link AbstractInternalListener} implementations. + * Every {@code InternalEvent} is identified by a unique {@link java.util.UUID}, which references the corresponding + * {@link de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentEvent} instance. + * + * @author Philipp Kunz + */ @Getter public abstract class InternalEvent extends ApplicationEvent { diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/NonTransientEventListenerException.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/NonTransientEventListenerException.java index 04e12305..8f9322ba 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/NonTransientEventListenerException.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/NonTransientEventListenerException.java @@ -1,5 +1,11 @@ package de.unistuttgart.iste.meitrex.gamification_service.events.internal; +/** + * Exception indicating a non-recoverable error during the processing of an internal event. Throwing this exception + * signals hat the event processing has failed permanently and should not be retried by the surrounding infrastructure. + * + * @see AbstractInternalListener + */ public class NonTransientEventListenerException extends InternalEventListenerException { public NonTransientEventListenerException() { diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/TransientEventListenerException.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/TransientEventListenerException.java index 424d3fbc..25e411a4 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/TransientEventListenerException.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/TransientEventListenerException.java @@ -1,5 +1,11 @@ package de.unistuttgart.iste.meitrex.gamification_service.events.internal; +/** + * Exception indicating a recoverable error during the processing of an internal event. Throwing this exception + * signals hat the event processing has permanently and should be retried by the surrounding infrastructure. + * + * @see AbstractInternalListener + */ public class TransientEventListenerException extends InternalEventListenerException { public TransientEventListenerException() { diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/persistent/PersistentEvent.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/persistent/PersistentEvent.java index 1b82c40a..0cac5db7 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/persistent/PersistentEvent.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/persistent/PersistentEvent.java @@ -9,6 +9,19 @@ import java.util.List; import java.util.UUID; +/** + * Base class for all persistently stored events within the gamification service. A PersistentEvent represents an + * externally received event that is stored permanently in the database. Each event has a unique UUID as its primary key, + * an optional message sequence number, and a timestamp indicating the time it was received. Each PersistentEvent can + * have multiple associated processing statuses, which are managed in the embedded PersistentEventStatus class. Such a + * status represents the processing state of an event for a specific listener. This allows the progress and outcome of + * event processing to be tracked per listener, including the current attempt count, the maximum number of allowed retries, + * and the timestamp of the last processing attempt. The class is declared abstract because concrete event types are + * defined through subclasses. These subclasses may contain additional domain-specific information required for further + * processing. + * + * @author Philipp Kunz + */ @Getter @Setter @SuperBuilder diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/publication/DefaultEventPublicationService.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/publication/DefaultEventPublicationService.java index 9f44b699..79bb15d0 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/publication/DefaultEventPublicationService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/publication/DefaultEventPublicationService.java @@ -18,6 +18,27 @@ import java.util.UUID; import java.util.function.Function; +/** + * Default implementation of {@link IEventPublicationService} that persists incoming persistent events, + * ensures that duplicate events are not processed, and publishes corresponding internal events + * after a successful transaction commit. + * + * This service uses a type-to-handler map to delegate the persistence and conversion logic + * for different event types. The {@link org.springframework.transaction.support.TransactionSynchronizationManager} + * is used to ensure that publication happens only after the surrounding database transaction commits successfully. + * + * Duplicate detection is applied only to events that implement the {@link de.unistuttgart.iste.meitrex.gamification_service.events.persistent.ISequenced} + * interface. For these events, the message sequence number is checked against the database to prevent reprocessing + * of duplicates. All other events are always treated as new and published unconditionally. + * + * To support additional event types, implement a corresponding persistence and conversion method (e.g. {@code saveMyEventType}), + * and register it in the {@code handlerMap} inside the constructor, mapping the new persistent event class + * to the handler function. This allows the service to process and publish the new event type without modifying + * the core publishing logic. + * + * @author Philipp Kunz + */ + @Slf4j @Component class DefaultEventPublicationService implements IEventPublicationService { diff --git a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/publication/IEventPublicationService.java b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/publication/IEventPublicationService.java index f77260eb..cfd37957 100755 --- a/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/publication/IEventPublicationService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/publication/IEventPublicationService.java @@ -2,8 +2,23 @@ import de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentEvent; +/** + * Defines the contract for publishing internal application events. Implementations are responsible for persisting + * events, ensuring that duplicate events are not processed, and publishing new events only after the surrounding + * transaction has been successfully committed. + * + * @author Philipp Kunz + */ public interface IEventPublicationService { + + /** + * Saves the given event if it has not been processed before and publishes it after the current transaction + * commits successfully as an instance of {@link de.unistuttgart.iste.meitrex.gamification_service.events.internal.InternalEvent}. + * + * @param persistentEvent the event to persist and publish (must not be {@code null}) + * @throws IllegalArgumentException if the event type is unsupported or the event is invalid + */ void saveCommitAndPublishIfNew(PersistentEvent persistentEvent); } From a5790ccfa43b32f9905db26b0ff8ef00d1e680ee Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Thu, 9 Oct 2025 21:17:55 +0200 Subject: [PATCH 3/3] Documentation complemented. --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/README.md b/README.md index 9fae2a3b..a4ce4852 100755 --- a/README.md +++ b/README.md @@ -34,6 +34,64 @@ The API documentation can be found in the wiki in the [API docs](api.md). The API is available at `/graphql` and the GraphiQL interface is available at `/graphiql`. +## Package Structure – `de.unistuttgart.iste.meitrex.gamification_service.service` + +The business logic of the Gamification Service is organized in the package +`de.unistuttgart.iste.meitrex.gamification_service.service`. +To ensure a clear architecture and separation of concerns, this package is divided into several **subpackages**: + +| Package | Purpose | +|-------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `de.unistuttgart.iste.meitrex.gamification_service.service.internal` | Contains **implementations of business logic** that are **not directly exposed via GraphQL**. These classes encapsulate internal application logic and are typically resolvers or event listeners. | +| `de.unistuttgart.iste.meitrex.gamification_service.service.functional` | Contains **functional, idempotent utility functions** that have no side effects. These classes and methods are usually stateless and can safely be reused throughout the application. | +| `de.unistuttgart.iste.meitrex.gamification_service.service.reactive` | Contains **event listeners** that extend `de.unistuttgart.iste.meitrex.gamification_service.events.internal.AbstractInternalListener`. These listeners react to **application events**, especially **external events that have been translated into internal events**. | + +### Implementation Guidelines +- **`internal`**: Use this package for services or components that implement complex business rules or calculations. These components may be used by various GraphQL resolvers or event listeners, but they should not handle transport or DTO transformations. +- **`functional`**: Use this package for small, easily testable functions that perform calculations, formatting, or transformations without modifying the application state. +- **`reactive`**: Listeners in this package are triggered via Spring Application Events and handle reactions to internal event streams. Typically, these correspond to **external events** that have been converted into **internal events** by a separate adapter layer. + +## Processing External Events + +External events are processed in three steps to ensure durability: + +1. **Reception & Mapping** + External events are received through Dapr pub/sub endpoints by listeners extending + `de.unistuttgart.iste.meitrex.gamification_service.dapr.AbstractExternalListener`. + These map incoming `CloudEvent` payloads to `PersistentEvent` objects and pass them to the `IEventPublicationService`. + +2. **Persistence & Publication** + The `DefaultEventPublicationService` persists each `PersistentEvent` and publishes a corresponding `InternalEvent` after the surrounding transaction commits. + Duplicate events (based on sequence numbers) are ignored. + +3. **Internal Event Handling** + Internal events (`de.unistuttgart.iste.meitrex.gamification_service.events.internal.InternalEvent`) are processed asynchronously by listeners extending + `de.unistuttgart.iste.meitrex.gamification_service.events.internal.AbstractInternalListener`. + These listeners: + - Load the persistent event from the database, + - Track processing status and retry attempts, + - Execute business logic in `doProcess(U persistentEvent)`, + - Distinguish between transient and non-transient failures. + +**Example Flow**: +`CloudEvent` → `AbstractExternalListener` → `PersistentEvent` stored → `InternalEvent` published → `AbstractInternalListener` processes domain logic. + +### Implementing a Custom Internal Listener + +To react to new internal events, developers must: + +1. **Create a `PersistentEvent` subclass** representing the data structure to be stored for the new external event. +2. **Extend `AbstractExternalListener`** to receive and map the external event to the new `PersistentEvent`. +3. **Register the new event type** in `DefaultEventPublicationService` by adding a mapping from the new `PersistentEvent` class to a function that saves it and creates a corresponding `InternalEvent`. +4. **Create an `InternalEvent` subclass** that references the persisted event’s UUID. +5. **Implement a new listener** by extending `AbstractInternalListener` and overriding: + - `getName()` with a unique, stable identifier, + - `doProcess(...)` with the actual business logic to run when the event is processed. +6. Optionally throw `TransientEventListenerException` for retryable errors or `NonTransientEventListenerException` for permanent failures. + +After these steps, the new internal listener will automatically react to published internal events of the specified type. + + ## Get started A guide how to start development can be found in the [wiki](https://meitrex.readthedocs.io/en/latest/dev-manuals/backend/get-started.html). \ No newline at end of file