summaryrefslogtreecommitdiff
path: root/20-implementierungsheft/assets/diagrams
diff options
context:
space:
mode:
authorOrangerot <purple@orangerot.dev>2024-05-24 17:42:08 +0200
committerOrangerot <purple@orangerot.dev>2024-05-24 17:47:22 +0200
commit7fcdc1c788725f866de71fc9dfd8c4d1cb132b57 (patch)
tree89931c85ae3f149884ba02c69862558e93f01531 /20-implementierungsheft/assets/diagrams
Initial commitHEADmain
Diffstat (limited to '20-implementierungsheft/assets/diagrams')
-rw-r--r--20-implementierungsheft/assets/diagrams/backendComponentDiagram.puml61
-rw-r--r--20-implementierungsheft/assets/diagrams/class_after.puml574
-rw-r--r--20-implementierungsheft/assets/diagrams/classdiagram.puml463
-rw-r--r--20-implementierungsheft/assets/diagrams/componentdiagram.puml79
-rw-r--r--20-implementierungsheft/assets/diagrams/db.puml78
-rw-r--r--20-implementierungsheft/assets/diagrams/deployment.puml59
-rw-r--r--20-implementierungsheft/assets/diagrams/gantt-plan.puml31
-rw-r--r--20-implementierungsheft/assets/diagrams/gantt-reality.puml39
-rw-r--r--20-implementierungsheft/assets/diagrams/sequencediagram-forgotAndResetPW.puml41
-rw-r--r--20-implementierungsheft/assets/diagrams/sequencediagram-getEpisodeActions.puml38
-rw-r--r--20-implementierungsheft/assets/diagrams/sequencediagram-getEpisodeActionsOfPodcastSince.puml32
-rw-r--r--20-implementierungsheft/assets/diagrams/sequencediagram-getSubscriptions.puml38
-rw-r--r--20-implementierungsheft/assets/diagrams/sequencediagram-register.puml26
-rw-r--r--20-implementierungsheft/assets/diagrams/sequencediagram-uploadEpisodeActions.puml38
-rw-r--r--20-implementierungsheft/assets/diagrams/sequencediagram-uploadSubscriptions.puml32
15 files changed, 1629 insertions, 0 deletions
diff --git a/20-implementierungsheft/assets/diagrams/backendComponentDiagram.puml b/20-implementierungsheft/assets/diagrams/backendComponentDiagram.puml
new file mode 100644
index 0000000..806522c
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/backendComponentDiagram.puml
@@ -0,0 +1,61 @@
+@startuml
+' skinparam linetype ortho
+
+'#########################################################################
+'SubscriptionsAPI
+component SubscriptionsAPI {
+
+ component SubscriptionService
+ component SubscriptionController
+ component SubscriptionDataAccessLayer
+
+ portout "Webserver" as wSub
+ portin "Database" as dSub
+ }
+
+dSub --0)- SubscriptionDataAccessLayer
+SubscriptionDataAccessLayer --0)- SubscriptionService
+SubscriptionService --0)- SubscriptionController
+SubscriptionController --0)- wSub
+
+'#########################################################################
+
+
+'#########################################################################
+'EpisodeActionsAPI
+
+component EpisodeActionsAPI {
+ component EpisodeActionService
+ component EpisodeActionController
+ component EpisodeActionDataAccessLayer
+
+ portout "Webserver" as wEpisode
+ portin "Database" as dEpisode
+}
+
+dEpisode --0)- EpisodeActionDataAccessLayer
+EpisodeActionController --0)- wEpisode
+EpisodeActionDataAccessLayer --0)- EpisodeActionService
+EpisodeActionService --0)- EpisodeActionController
+
+'#########################################################################
+
+
+'#########################################################################
+'AuthenticationAPI
+
+component AuthenticationAPI {
+ component AuthenticationService
+ component AuthenticationController
+ component AuthenticationDataAccessLayer
+
+ portout "Webserver" as wAuth
+ portin "Database" as dAuth
+}
+
+dAuth --0)- AuthenticationDataAccessLayer
+AuthenticationController --0)- wAuth
+AuthenticationDataAccessLayer --0)- AuthenticationService
+AuthenticationService --0)- AuthenticationController
+
+@enduml
diff --git a/20-implementierungsheft/assets/diagrams/class_after.puml b/20-implementierungsheft/assets/diagrams/class_after.puml
new file mode 100644
index 0000000..0a8f475
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/class_after.puml
@@ -0,0 +1,574 @@
+@startuml
+' skinparam linetype ortho
+' skinparam groupInheritance 2
+allowmixing
+
+package authentication_api <<Frame>> {
+
+ package controller as auth.controller <<Frame>> {
+
+ class AuthenticationController <<@RestController>> {
+ + <<create>> AuthenticationController(AuthenticationService, EMailConfigProperties):
+ + logout(String, HttpServletResponse): HttpStatus
+ + registerUser(UserInfoRequest): HttpStatus
+ + forgotPassword(String): HttpStatus
+ + getDeviceList(String, HttpServletResponse): ResponseEntity<List<DeviceWrapper>>
+ + login(String, HttpServletResponse): HttpStatus
+ + deleteUser(String, PasswordRequest): HttpStatus
+ + changePassword(String, ChangePasswordRequest): HttpStatus
+ + verifyRegistration(String, String, HttpServletResponse): HttpStatus
+ + resetPassword(String, String, PasswordRequest): HttpStatus
+ }
+
+ entity AuthenticationResponse << record >> {
+ + <<create>> AuthenticationResponse(String):
+ + token(): String
+ }
+
+ entity ChangePasswordRequest << record >> {
+ + <<create>> ChangePasswordRequest(String, String):
+ + oldPassword(): String
+ + newPassword(): String
+ }
+
+ entity DeviceWrapper << record >> {
+ + <<create>> DeviceWrapper():
+ + <<create>> DeviceWrapper(String, String, String, int):
+ + id(): String
+ + caption(): String
+ + type(): String
+ + subscriptions(): int
+ }
+
+ entity ForgotPasswordRequest << record >> {
+ + <<create>> ForgotPasswordRequest(String):
+ + email(): String
+ }
+
+ entity PasswordRequest << record >> {
+ + <<create>> PasswordRequest(String):
+ + password(): String
+ }
+
+ entity UserInfoRequest << record >> {
+ + <<create>> UserInfoRequest(String, String, String):
+ + password(): String
+ + email(): String
+ + username(): String
+ }
+ }
+
+ package data_access as auth.dao <<Frame>> {
+
+ interface AuthenticationDao <<@Repository>> {
+ + findByEmail(String): Optional<User>
+ + deleteAllByEnabledFalseAndCreatedAtLessThan(long): void
+ + existsByUsername(String): boolean
+ + findByUsername(String): Optional<User>
+ }
+ }
+
+ package service as auth.service <<Frame>> {
+
+ class AuthenticationService <<@Service>> {
+ + <<create>> AuthenticationService(AuthenticationDao, PasswordEncoder, JwtService, EMailServiceImpl, EncryptionService, InputCheckService):
+ + verifyRegistration(String, String): HttpStatus
+ + logout(String, HttpServletResponse): HttpStatus
+ + changePassword(String, ChangePasswordRequest): HttpStatus
+ + registerUser(UserInfoRequest): HttpStatus
+ + deleteUser(String, PasswordRequest): HttpStatus
+ + forgotPassword(String): HttpStatus
+ + resetPassword(String, String, PasswordRequest): HttpStatus
+ + deleteInvalidUsersOlderThan(long): void
+ + login(String, HttpServletResponse): HttpStatus
+ }
+
+ class EMailServiceImpl <<@Service>> {
+ + <<create>> EMailServiceImpl(JavaMailSender, EMailConfigProperties, JwtService):
+ - substitutePlaceholders(String, UserDetails, String): String
+ + sendVerification(String, UserDetails): void
+ + sendPasswordReset(String, UserDetails): void
+ - generatePasswordResetURLString(UserDetails): String
+ - sendMail(String, String, String): void
+ - generateVerificationURLString(UserDetails): String
+ }
+
+ class EncryptionService <<@Service>> {
+ + <<create>> EncryptionService(SecurityConfigProperties):
+ + saltAndHashEmail(String): String
+ - getSalt(): byte[]
+ }
+
+ class InputCheckService <<@Service>> {
+ + <<create>> InputCheckService():
+ + validateEmail(String): boolean
+ + validateUsername(String): boolean
+ + validatePassword(String): boolean
+ }
+
+ class ResourceReader {
+ + <<create>> ResourceReader():
+ + readFileToString(String): String
+ }
+ }
+}
+
+package config <<Frame>> {
+
+ class ApplicationConfig <<@Configuration>> {
+ + <<create>> ApplicationConfig(AuthenticationDao):
+ + userDetailsService(): UserDetailsService
+ + addInterceptors(InterceptorRegistry): void
+ + authenticationManager(AuthenticationConfiguration): AuthenticationManager
+ + passwordEncoder(): PasswordEncoder
+ + corsConfigurer(): WebMvcConfigurer
+ + authenticationProvider(): AuthenticationProvider
+ }
+
+ class AuthenticationValidatorInterceptor {
+ + <<create>> AuthenticationValidatorInterceptor():
+ - extractUsernamePathVariable(HttpServletRequest): String?
+ + preHandle(HttpServletRequest, HttpServletResponse, Object): boolean
+ }
+
+ entity EMailConfigProperties << record >> {
+ + <<create>> EMailConfigProperties(String, String, String):
+ + resetUrlPath(): String
+ + verificationUrl(): String
+ + dashboardBaseUrl(): String
+ }
+
+ class JwtAuthenticationFilter <<@Component>> {
+ + <<create>> JwtAuthenticationFilter(JwtService, UserDetailsService):
+ # doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain): void
+ - authenticateIfValid(Cookie, HttpServletRequest): void
+ }
+
+ class JwtService <<@Service>> {
+ + <<create>> JwtService(SecurityConfigProperties):
+ + generateUrlTokenString(UserDetails): String
+ + isTokenValid(String, UserDetails): boolean
+ + generateAccessTokenString(UserDetails): String
+ + extractUsername(String): String
+ + extractClaim(String, Function<Claims, T>): T
+ - isTokenExpired(String): boolean
+ + generateTokenString(Map<String, Object>, UserDetails, long): String
+ - extractExpiration(String): Date
+ - extractAllClaims(String): Claims
+ - getSigningKey(): Key
+ }
+
+ class SecurityConfig <<@Configuration>> {
+ + <<create>> SecurityConfig(JwtAuthenticationFilter, AuthenticationProvider):
+ ~ corsConfigurationSource(): CorsConfigurationSource
+ + securityFilterChain(HttpSecurity): SecurityFilterChain
+ }
+
+ entity SecurityConfigProperties << record >> {
+ + <<create>> SecurityConfigProperties(String, String):
+ + jwtSigningKey(): String
+ + emailSigningKey(): String
+ }
+}
+
+package episode_actions_api <<Frame>> {
+
+ package controller as episode.controller <<Frame>> {
+
+ class EpisodeActionController <<@RestController>> {
+ + <<create>> EpisodeActionController(EpisodeActionService):
+ + addEpisodeActions(String, List<EpisodeActionPost>): ResponseEntity<UpdateURLsWrapper>
+ + getEpisodeActionsOfPodcast(String, String): ResponseEntity<EpisodeActionGetResponse>
+ + getEpisodeActions(String): ResponseEntity<EpisodeActionGetResponse>
+ + getEpisodeActionsSince(String, long): ResponseEntity<EpisodeActionGetResponse>
+ + getEpisodeActionsOfPodcastSince(String, String, long): ResponseEntity<EpisodeActionGetResponse>
+ }
+
+ class EpisodeActionGetResponse {
+ + <<create>> EpisodeActionGetResponse(List<EpisodeActionPost>):
+ + getTimestamp(): long
+ + getActions(): List<EpisodeActionPost>
+ }
+
+ class EpisodeActionPost {
+ + <<create>> EpisodeActionPost():
+ + <<create>> EpisodeActionPost(String, String, String, String, int, EpisodeAction):
+ + equals(Object): boolean
+ + hashCode(): int
+ + builder(): EpisodeActionPostBuilder
+ + getPodcastURL(): String
+ + getEpisodeURL(): String
+ + getTitle(): String
+ + getGuid(): String
+ + setGuid(String): void
+ + setEpisodeAction(EpisodeAction): void
+ + setTotal(int): void
+ + getTotal(): int
+ + setTitle(String): void
+ # canEqual(Object): boolean
+ + getEpisodeAction(): EpisodeAction
+ + toString(): String
+ + setPodcastURL(String): void
+ + setEpisodeURL(String): void
+ }
+ }
+
+ package data_access as episode.dao <<Frame>> {
+
+ interface EpisodeActionDao <<@Repository>> {
+ + findByUserUsernameAndEpisodeSubscriptionUrl(String, String): List<EpisodeAction>
+ + delete(EpisodeAction): void
+ + findByUserUsernameAndTimestampGreaterThanEqual(String, LocalDateTime): List<EpisodeAction>
+ + findByUserUsername(String): List<EpisodeAction>
+ + findByUserAndEpisodeUrlAndAction(User, String, Action): Optional<EpisodeAction>
+ + findByUserUsernameAndTimestampGreaterThanEqualAndEpisodeSubscriptionUrl(String, LocalDateTime, String): List<EpisodeAction>
+ + deleteByUserUsernameAndEpisodeSubscriptionUrl(String, String): void
+ + existsByUserAndEpisodeUrlAndAction(User, String, Action): boolean
+ }
+
+ interface EpisodeDao <<@Repository>> {
+ + existsByGuid(String): boolean
+ + findByUrl(String): Optional<Episode>
+ + existsByUrl(String): boolean
+ + findByGuid(String): Optional<Episode>
+ }
+ }
+
+ package service as episode.service <<Frame>> {
+
+ class EpisodeActionService <<@Service>> {
+ + <<create>> EpisodeActionService(EpisodeActionDao, EpisodeDao, AuthenticationDao, SubscriptionDao, SubscriptionActionDao, RSSParser):
+ - episodeActionPostsToEpisodeActions(User, List<EpisodeActionPost>): List<EpisodeAction>
+ - createEpisode(EpisodeActionPost): Episode
+ + getEpisodeActionsOfPodcastSince(String, String, long): List<EpisodeActionPost>
+ - getEpisodeFromDatabase(EpisodeActionPost): Episode
+ - addEpisodeActionsToDatabase(User, List<EpisodeAction>): void
+ - addNewestEpisodeActionToDatabase(User, EpisodeAction): void
+ - episodeActionsToEpisodeActionPosts(List<EpisodeAction>): List<EpisodeActionPost>
+ - addEpisodeActionToDatabase(User, EpisodeAction): void
+ + getEpisodeActionsSince(String, long): List<EpisodeActionPost>
+ - episodeActionPostToEpisodeAction(User, EpisodeActionPost): EpisodeAction
+ + getEpisodeActions(String): List<EpisodeActionPost>
+ + getEpisodeActionsOfPodcast(String, String): List<EpisodeActionPost>
+ + addEpisodeActions(String, List<EpisodeActionPost>): void
+ }
+ }
+}
+
+package model <<Frame>> {
+
+ enum Action << enumeration >> {
+ + <<create>> Action():
+ + valueOf(String): Action
+ + getJsonProperty(): String
+ + values(): Action[]
+ }
+
+ class Episode <<@Entity>> {
+ + <<create>> Episode():
+ + <<create>> Episode(Long, String, String, String, int, Subscription, List<EpisodeAction>):
+ + getId(): Long
+ # canEqual(Object): boolean
+ + getGuid(): String
+ + setTitle(String): void
+ + builder(): EpisodeBuilder
+ + setTotal(int): void
+ + setGuid(String): void
+ + equals(Object): boolean
+ + setSubscription(Subscription): void
+ + getUrl(): String
+ + getTitle(): String
+ + toString(): String
+ + getTotal(): int
+ + setUrl(String): void
+ + setEpisodeActions(List<EpisodeAction>): void
+ + getSubscription(): Subscription
+ + getEpisodeActions(): List<EpisodeAction>
+ + setId(Long): void
+ + hashCode(): int
+ }
+
+ class EpisodeAction <<@Entity>> {
+ + <<create>> EpisodeAction(Long, User, Episode, LocalDateTime, Action, int, int):
+ + <<create>> EpisodeAction():
+ + getEpisode(): Episode
+ + setEpisode(Episode): void
+ + setPosition(int): void
+ # canEqual(Object): boolean
+ + setUser(User): void
+ + setStarted(int): void
+ + getId(): Long
+ + getUser(): User
+ + getTimestamp(): LocalDateTime
+ + getAction(): Action
+ + getStarted(): int
+ + hashCode(): int
+ + setTimestamp(LocalDateTime): void
+ + equals(Object): boolean
+ + getPosition(): int
+ + builder(): EpisodeActionBuilder
+ + setId(Long): void
+ + setAction(Action): void
+ + toString(): String
+ + toEpisodeActionPost(): EpisodeActionPost
+ }
+
+ enum Role << enumeration >> {
+ + <<create>> Role():
+ + valueOf(String): Role
+ + toString(): String
+ + values(): Role[]
+ }
+
+ class Subscription <<@Entity>> {
+ + <<create>> Subscription():
+ + <<create>> Subscription(Long, String, String, long, List<SubscriptionAction>, List<Episode>):
+ + setId(Long): void
+ + getId(): Long
+ + equals(Object): boolean
+ + getUrl(): String
+ + hashCode(): int
+ + builder(): SubscriptionBuilder
+ # canEqual(Object): boolean
+ + toString(): String
+ + getTitle(): String
+ + getTimestamp(): long
+ + setEpisodes(List<Episode>): void
+ + getSubscriptionActions(): List<SubscriptionAction>
+ + getEpisodes(): List<Episode>
+ + setSubscriptionActions(List<SubscriptionAction>): void
+ + setUrl(String): void
+ + setTitle(String): void
+ + setTimestamp(long): void
+ + addEpisode(Episode): void
+ }
+
+ class SubscriptionAction <<@Entity>> {
+ + <<create>> SubscriptionAction():
+ + <<create>> SubscriptionAction(int, User, long, Subscription, boolean):
+ + equals(Object): boolean
+ + getId(): int
+ + getUser(): User
+ + getTimestamp(): long
+ + getSubscription(): Subscription
+ + isAdded(): boolean
+ # canEqual(Object): boolean
+ + setId(int): void
+ + hashCode(): int
+ + setUser(User): void
+ + toString(): String
+ + builder(): SubscriptionActionBuilder
+ + setTimestamp(long): void
+ + setSubscription(Subscription): void
+ + setAdded(boolean): void
+ }
+
+ class User <<@Entity>> {
+ + <<create>> User(Long, String, String, String, boolean, long, Role, List<SubscriptionAction>, List<EpisodeAction>):
+ + <<create>> User():
+ + getId(): Long
+ + setCreatedAt(long): void
+ + getUsername(): String
+ + builder(): UserBuilder
+ + toString(): String
+ + getEmail(): String
+ + setPassword(String): void
+ + setSubscriptionActions(List<SubscriptionAction>): void
+ + equals(Object): boolean
+ + getPassword(): String
+ + setEmail(String): void
+ + setRole(Role): void
+ + isEnabled(): boolean
+ + setUsername(String): void
+ + getCreatedAt(): long
+ + getRole(): Role
+ + getSubscriptionActions(): List<SubscriptionAction>
+ # canEqual(Object): boolean
+ + hashCode(): int
+ + setEnabled(boolean): void
+ + setEpisodeActions(List<EpisodeAction>): void
+ + getEpisodeActions(): List<EpisodeAction>
+ + setId(Long): void
+ + getAuthorities(): Collection<GrantedAuthority>
+ + isCredentialsNonExpired(): boolean
+ + isAccountNonLocked(): boolean
+ + isAccountNonExpired(): boolean
+ }
+}
+
+package subscriptions_api <<Frame>> {
+
+ package controller as subscription.controller <<Frame>> {
+
+ class SubscriptionController <<@RestController>> {
+ + <<create>> SubscriptionController(SubscriptionService):
+ + applySubscriptionDelta(String, String, SubscriptionDelta): ResponseEntity<UpdateURLsWrapper>
+ + getSubscriptions(String, String, String): ResponseEntity<List<String>>
+ + getSubscriptionDelta(String, String, long): ResponseEntity<SubscriptionDelta>
+ + getTitles(String): ResponseEntity<List<SubscriptionTitles>>
+ + uploadSubscriptions(String, String, List<String>): ResponseEntity<String>
+ }
+
+ class SubscriptionDelta {
+ + <<create>> SubscriptionDelta(List<String>, List<String>):
+ + getTimestamp(): long
+ + getRemove(): List<String>
+ + getAdd(): List<String>
+ }
+
+ entity SubscriptionTitles << record >> {
+ + <<create>> SubscriptionTitles(Subscription, List<EpisodeActionPost>):
+ + episodes(): List<EpisodeActionPost>
+ + subscription(): Subscription
+ }
+ }
+
+ package data_access as subscription.dao <<Frame>> {
+
+ interface SubscriptionActionDao <<@Repository>> {
+ + findByUserUsernameAndAddedTrue(String): List<SubscriptionAction>
+ + existsByUserAndSubscription(User, Subscription): boolean
+ + findByUserAndSubscription(User, Subscription): Optional<SubscriptionAction>
+ + findByUserUsernameAndTimestampGreaterThanEqual(String, long): List<SubscriptionAction>
+ + findByUserUsernameAndAddedTrueAndTimestampGreaterThanEqual(String, long): List<SubscriptionAction>
+ }
+
+ interface SubscriptionDao <<@Repository>> {
+ + findByUrl(String): Optional<Subscription>
+ + existsByUrl(String): boolean
+ }
+ }
+
+ package service as subscription.service <<Frame>> {
+
+ class SubscriptionService <<@Service>> {
+ + <<create>> SubscriptionService(RSSParser, AuthenticationDao, SubscriptionDao, SubscriptionActionDao, EpisodeActionDao, EpisodeActionService):
+ + getTitles(String): List<SubscriptionTitles>
+ + getSubscriptions(String): List<String>
+ + applySubscriptionDelta(String, SubscriptionDelta): int
+ + getSubscriptionDelta(String, long): SubscriptionDelta
+ + uploadSubscriptions(String, List<String>): int
+ }
+ }
+}
+
+package util <<Frame>> {
+
+ class RSSParser <<@Component>> {
+ + <<create>> RSSParser(EpisodeDao, SubscriptionDao):
+ + validate(Subscription): void
+ - parseTimeToSeconds(String): int
+ - parseEpisode(SyndEntry, Subscription): Episode
+ - saveEpisodes(List<Episode>): void
+ - fetchSubscriptionFeed(Subscription): Map<String, Episode>?
+ - saveSubscription(Subscription): void
+ - deleteSubscription(Subscription): void
+ - getFetchedEpisodeForURL(String, Map<String, Episode>): Episode
+ - deleteEpisodes(List<Episode>): void
+ }
+
+ class Scheduler <<@Component>> {
+ + <<create>> Scheduler():
+ + clean(): void
+ }
+
+ class UpdateURLsWrapper {
+ + <<create>> UpdateURLsWrapper():
+ + getTimestamp(): long
+ + getUpdateURLs(): List<Pair<String, String>>
+ }
+}
+
+class ServerApplication <<@SpringBootApplication>> {
+ + <<create>> ServerApplication():
+ + main(String[]): void
+}
+
+database Datenbank
+Datenbank <-[hidden]d- subscriptions_api
+Datenbank <-[hidden]d- episode_actions_api
+Datenbank <-[hidden]d- authentication_api
+() SQL as SQLSub
+() SQL as SQLAuth
+() SQL as SQLEpisode
+
+Datenbank -- SQLSub
+Datenbank -- SQLAuth
+Datenbank -- SQLEpisode
+
+SubscriptionController ..o ServerApplication
+AuthenticationController ..o ServerApplication
+EpisodeActionController ..o ServerApplication
+
+ServerApplication --() HTTP
+
+SQLSub )-- SubscriptionActionDao: JPA
+SQLSub )-- SubscriptionDao: JPA
+SQLAuth )-- AuthenticationDao: JPA
+SQLEpisode )-- EpisodeActionDao: JPA
+SQLEpisode )-- EpisodeDao: JPA
+
+model .o Datenbank: ORM (User, SubscriptionAction, Subscription, EpisodeAction, Episode)
+' Datenbank o.. Subscription: ORM
+' Datenbank o.. SubscriptionAction: ORM
+' Datenbank o.. Episode: ORM
+' Datenbank o.. EpisodeAction: ORM
+' Datenbank o.. User: ORM
+
+ApplicationConfig "1" *-[#595959,plain]-> "authenticationDao\n1" AuthenticationDao
+ApplicationConfig -[#595959,dashed]-> AuthenticationValidatorInterceptor : "«create»"
+AuthenticationController "1" *-[#595959,plain]-> "authenticationService\n1" AuthenticationService
+AuthenticationController -[#595959,dashed]-> DeviceWrapper : "«create»"
+AuthenticationController "1" *-[#595959,plain]-> "eMailConfigProperties\n1" EMailConfigProperties
+AuthenticationService "1" *-[#595959,plain]-> "authenticationDao\n1" AuthenticationDao
+AuthenticationService "1" *-[#595959,plain]-> "eMailService\n1" EMailServiceImpl
+AuthenticationService "1" *-[#595959,plain]-> "encryptionService\n1" EncryptionService
+AuthenticationService "1" *-[#595959,plain]-> "inputCheckService\n1" InputCheckService
+AuthenticationService "1" *-[#595959,plain]-> "jwtService\n1" JwtService
+AuthenticationService "1" *-[#595959,plain]-> "DEFAULT_USER\n1" Role
+EMailServiceImpl "1" *-[#595959,plain]-> "eMailConfigProperties\n1" EMailConfigProperties
+EMailServiceImpl "1" *-[#595959,plain]-> "jwtService\n1" JwtService
+EncryptionService "1" *-[#595959,plain]-> "securityConfigProperties\n1" SecurityConfigProperties
+Episode "1" *-[#595959,plain]-> "episodeActions\n*" EpisodeAction
+Episode "1" *-[#595959,plain]-> "subscription\n1" Subscription
+EpisodeAction "1" *-[#595959,plain]-> "action\n1" Action
+EpisodeAction "1" *-[#595959,plain]-> "episode\n1" Episode
+EpisodeAction -[#595959,dashed]-> EpisodeActionPost : "«create»"
+EpisodeAction "1" *-[#595959,plain]-> "user\n1" User
+EpisodeActionController -[#595959,dashed]-> EpisodeActionGetResponse : "«create»"
+EpisodeActionController "1" *-[#595959,plain]-> "episodeActionService\n1" EpisodeActionService
+EpisodeActionController -[#595959,dashed]-> UpdateURLsWrapper : "«create»"
+EpisodeActionGetResponse "1" *-[#595959,plain]-> "actions\n*" EpisodeActionPost
+EpisodeActionPost "1" *-[#595959,plain]-> "episodeAction\n1" EpisodeAction
+EpisodeActionService "1" *-[#595959,plain]-> "authenticationDao\n1" AuthenticationDao
+EpisodeActionService "1" *-[#595959,plain]-> "episodeActionDao\n1" EpisodeActionDao
+EpisodeActionService "1" *-[#595959,plain]-> "episodeDao\n1" EpisodeDao
+EpisodeActionService "1" *-[#595959,plain]-> "rssParser\n1" RSSParser
+EpisodeActionService -[#595959,dashed]-> Subscription : "«create»"
+EpisodeActionService "1" *-[#595959,plain]-> "subscriptionActionDao\n1" SubscriptionActionDao
+EpisodeActionService "1" *-[#595959,plain]-> "subscriptionDao\n1" SubscriptionDao
+JwtAuthenticationFilter "1" *-[#595959,plain]-> "jwtService\n1" JwtService
+JwtService "1" *-[#595959,plain]-> "securityConfigProperties\n1" SecurityConfigProperties
+RSSParser "1" *-[#595959,plain]-> "episodeDao\n1" EpisodeDao
+RSSParser "1" *-[#595959,plain]-> "subscriptionDao\n1" SubscriptionDao
+Scheduler "1" *-[#595959,plain]-> "authenticationService\n1" AuthenticationService
+SecurityConfig "1" *-[#595959,plain]-> "jwtAuthFilter\n1" JwtAuthenticationFilter
+Subscription "1" *-[#595959,plain]-> "episodes\n*" Episode
+Subscription "1" *-[#595959,plain]-> "subscriptionActions\n*" SubscriptionAction
+SubscriptionAction "1" *-[#595959,plain]-> "subscription\n1" Subscription
+SubscriptionAction "1" *-[#595959,plain]-> "user\n1" User
+SubscriptionController "1" *-[#595959,plain]-> "subscriptionService\n1" SubscriptionService
+SubscriptionController -[#595959,dashed]-> UpdateURLsWrapper : "«create»"
+SubscriptionService "1" *-[#595959,plain]-> "authenticationDao\n1" AuthenticationDao
+SubscriptionService "1" *-[#595959,plain]-> "episodeActionDao\n1" EpisodeActionDao
+SubscriptionService "1" *-[#595959,plain]-> "episodeActionService\n1" EpisodeActionService
+SubscriptionService "1" *-[#595959,plain]-> "rssParser\n1" RSSParser
+SubscriptionService "1" *-[#595959,plain]-> "subscriptionActionDao\n1" SubscriptionActionDao
+SubscriptionService "1" *-[#595959,plain]-> "subscriptionDao\n1" SubscriptionDao
+SubscriptionService -[#595959,dashed]-> SubscriptionDelta : "«create»"
+SubscriptionService -[#595959,dashed]-> SubscriptionTitles : "«create»"
+SubscriptionTitles "1" *-[#595959,plain]-> "subscription\n1" Subscription
+User "1" *-[#595959,plain]-> "episodeActions\n*" EpisodeAction
+User "1" *-[#595959,plain]-> "role\n1" Role
+User "1" *-[#595959,plain]-> "subscriptionActions\n*" SubscriptionAction
+@enduml
diff --git a/20-implementierungsheft/assets/diagrams/classdiagram.puml b/20-implementierungsheft/assets/diagrams/classdiagram.puml
new file mode 100644
index 0000000..4b1970a
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/classdiagram.puml
@@ -0,0 +1,463 @@
+@startuml
+' skinparam linetype ortho
+' skinparam groupInheritance 2
+allowmixing
+
+package subscriptionsAPI <<Frame>> {
+ package subscriptionDataAccessLayer <<Frame>> {
+ class SubscriptionDataAccessService <<@Repository>> {
+ <<create>> SubscriptionDataAccessService(JpaTemplate jpaTemplate)
+ int uploadSubscriptions(String username, List<SubscriptionAction> subscriptions)
+ List<String> getSubscriptions(String username)
+ List<String> getSubscriptionsSince(String username, LocalDateTime time)
+ int addSubscriptions(String username, List<SubscriptionAction> addedSubscriptions)
+ int removeSubscriptions(String username, List<SubscriptionAction> removedSubscriptions)
+ List<SubscriptionTitles> getTitles(String username)
+ }
+
+ interface SubscriptionDao {
+ int uploadSubscriptions(String username, List<SubscriptionAction> subscriptions)
+ List<String> getSubscriptions(String username)
+ List<String> getSubscriptionsSince(String username, LocalDateTime time)
+ int addSubscriptions(String username, List<SubscriptionAction> addedSubscriptions)
+ int removeSubscriptions(String username, List<SubscriptionAction> removedSubscriptions)
+ List<SubscriptionTitles> getTitles(String username)
+ }
+ }
+
+ package subscriptionService <<Frame>> {
+ class SubscriptionService <<@Service>> {
+ <<create>> SubscriptionService(SubscriptionDao subscriptionDao)
+ int uploadSubscriptions(String username, List<SubscriptionAction> subscriptions)
+ List<String> getSubscriptions(String username)
+ List<String> getSubscriptionsSince(String username, LocalDateTime time)
+ int addSubscriptions(String username, List<SubscriptionAction> addedSubscriptions)
+ int removeSubscriptions(String username, List<SubscriptionAction> removedSubscriptions)
+ List<SubscriptionTitles> getTitles(String username)
+ }
+ }
+
+ package subscriptionController <<Frame>> {
+ class SubscriptionController <<@Controller>>{
+ ' @Autowired
+ <<create>> SubscriptionController(SubscriptionService subscriptionService)
+ ' @GetMapping
+ ResponseEntity<List<String>> getSubscriptions(String username, String deviceID, String functionJSONP)
+ ' @PutMapping
+ ResponseEntity<String> uploadSubscriptions(String username, String deviceID, List<String> subscriptions)
+ ' @PostMapping
+ ResponseEntity<SubscriptionDelta> applySubscriptionDelta(String username, String deviceID, SubscriptionDelta delta)
+ ' @GetMapping
+ ResponseEntity<SubscriptionDelta> getSubscriptionDelta(String username, String deviceID, long since)
+ ResponseEntity<List<SubscriptionTitles>> getTitles(String username, String deviceID)
+ }
+
+ class SubscriptionTitles {
+ <<create>> SubscriptionTitles(Subscription subscription, List<EpisodeActionPost> episodeTitles)
+ Subscription getSubscription()
+ List<EpisodeActionPost> getEpisodesTitles()
+ }
+
+ class SubscriptionDelta {
+ <<create>> SubscriptionDelta(List<String> add, List<String> remove)
+ List<String> getRemove()
+ LocalDate getTimestamp()
+ List<List<String>> getUpdate_urls()
+ }
+ }
+
+}
+
+package episodeActionsAPI <<Frame>> {
+ package episodeActionDataAccessLayer <<Frame>> {
+ class EpisodeActionDataAccessService <<@Repository>> {
+ <<create>> EpisodeActionDataAccessService (JpaTemplate jpaTemplate)
+ long addEpisodeActions(String username, List<EpisodeActionPost> episodeActionPosts)
+ List<EpisodeActionPost> getEpisodeActions(String username)
+ List<EpisodeActionPost> getEpisodeActionsOfPodcast(String username, String podcastURL)
+ List<EpisodeActionPost> getEpisodeActionsSince(String username, LocalDateTime since)
+ List<EpisodeActionPost> getEpisodeActionsOfPodcastSince(String username, String podcastURL, LocalDateTime since)
+ }
+
+ interface EpisodeActionDao {
+ long addEpisodeActions(String username, List<EpisodeActionPost> episodeActionPosts)
+ List<EpisodeActionPost> getEpisodeActions(String username)
+ List<EpisodeActionPost> getEpisodeActionsOfPodcast(String username, String podcastURL)
+ List<EpisodeActionPost> getEpisodeActionsSince(String username, LocalDateTime since)
+ List<EpisodeActionPost> getEpisodeActionsOfPodcastSince(String username, String podcastURL, LocalDateTime since)
+ }
+ }
+
+ package episodeActionService <<Frame>> {
+ class EpisodeActionService <<@Service>> {
+ <<create>> EpisodeActionService (EpisodeActionDao episodeActionDao)
+ LocalDateTime addEpisodeActions(String username, List<EpisodeActionPosts> episodeActionPosts)
+ List<EpisodeActionPost> getEpisodeActions(String username)
+ List<EpisodeActionPost> getEpisodeActionsOfPodcast(String username, String podcastURL)
+ List<EpisodeActionPost> getEpisodeActionsSince(String username, LocalDateTime since)
+ List<EpisodeActionPost> getEpisodeActionsOfPodcastSince(String username, String podcastURL, LocalDateTime since)
+ }
+ }
+
+ package episodeActionController <<Frame>> {
+ class EpisodeActionController <<@Controller>>{
+ <<create>> EpisodeActionController (EpisodeActionService episodeActionService)
+ ResponseEntity<EpisodeActionPostResponse> addEpisodeActions(String username, EpisodeActionPostRequest episodeActionPostRequest)
+ ResponseEntity<EpisodeActionGetResponse> getEpisodeActions(String username, String deviceID, boolean aggregated)
+ ResponseEntity<EpisodeActionGetResponse> getEpisodeActionsOfPodcast(String username, String podcastURL, String deviceID, boolean aggregated)
+ ResponseEntity<EpisodeActionGetResponse> getEpisodeActionsSince(String username, String deviceID, long since, boolean aggregated)
+ ResponseEntity<EpisodeActionGetResponse> getEpisodeActionsOfPodcastSince(String username, String podcastURL, String deviceID, long since, boolean aggregated)
+ }
+
+ class EpisodeActionPostResponse {
+ <<create>> EpisodeActionPostResponse(List<Pair<String, String>> updateURLs)
+ long getTimestamp()
+ List<Pair<String, String>> getUpdatedURLs()
+ }
+
+ class EpisodeActionPost {
+ <<create>> EpisodeActionPost(String podcastURL, String episodeURL, Action action, LocalDateTime timestamp, int started, int position)
+ String getPodcastURL()
+ String getEpisodeURL()
+ int getGUID()
+ Action getAction()
+ LocalDateTime getTimestamp()
+ int getStarted()
+ int getPosition()
+ EpisodeAction getEpisodeAction()
+ }
+
+ class EpisodeActionPostRequest {
+ <<create>> EpisodeActionPostRequest(List<EpisodeActionPost> episodeActionPosts)
+ List<EpisodeActionPost> getEpisodeActionPosts()
+ }
+
+ class EpisodeActionGetResponse {
+ <<create>> EpisodeActionGetResponse(List<EpisodeActionPost> episodeActionPosts)
+ List<EpisodeActionPost> getEpisodeActionPosts()
+ long getTimestamp()
+ }
+ }
+}
+
+package authenticationAPI <<Frame>> {
+ package authenticationDataAccessLayer <<Frame>> {
+ ' interface AuthenticationDao {
+ ' String login(String username)
+ ' int logout(String username)
+ ' }
+
+ ' class AuthenticationDataAccessService <<@Respository>> {
+ ' <<create>> AuthenticationDataAccessService(JpaTemplate jpaTemplate)
+ ' String login(String username)
+ ' int logout(String username)
+ ' }
+
+ interface UserDetailsManager {
+ void createUser(UserDetails userDetails)
+ void changePassword(String oldPassword, String newPassword)
+ void deleteUser(String username)
+ void updateUser(UserDetails user)
+ boolean userExists(String username)
+ }
+ note left
+ Aus org.springframework.security.provisioning
+ - liefert Methoden zum Erstellen neuer User
+ und zum Aktualisieren bestehender.
+ end note
+
+ class JdbcUserDetailsManager <<@Repository>> {
+ <<create>> JdbcUserDetailsManager(DataSource dataSource)
+ void createUser(UserDetails user)
+ void changePassword(String oldPassword, String newPassword)
+ void deleteUser(String username)
+ void updateUser(UserDetails user)
+ boolean userExists(String username)
+ }
+ note right
+ User Management Service aus dem Paket
+ org.springframework.security.provisioning
+ der CRUD Operationen für User bereitstellt.
+ Hier sind nur die relevanten Methoden modelliert.
+ end note
+ }
+
+ package authenticationService <<Frame>> {
+ class AuthenticationService <<@Service>> {
+ --
+ <<create>> AuthenticationService(UserDetailsManager userDetailsManager)
+ List<String> verifyLogin(String username)
+ int logout(String username)
+ int forgotPassword(ForgotPasswordRequest forgotPasswordRequest)
+ .. via JdbcUserDetailsManager ..
+ int resetPassword(String username, RequestWithPassword requestWithPassword)
+ int registerUser(UserDetails user)
+ int changePassword(String username, ChangePasswordRequest changePasswordRequest)
+ int deleteUser(String username, RequestWithPassword requestWithPassword)
+ }
+
+ class JavaMailSenderImpl {}
+ note left
+ Aus org.springframework.mail.javamail.
+ Implementierung des JavaMailSender Interfaces,
+ welches das MailSender Interface durch Unterstützung
+ von MIME Nachrichten erweitert.
+ Das MailSender Interface definiert dabei eine
+ Strategie zum Versenden einfacher Mails.
+ Unterstützt sowohl JavaMail MimeMessages und
+ Spring SimpleMailMessages.
+ end note
+ }
+
+ package authenticationController <<Frame>> {
+ class AuthenticationController <<@Controller>> {
+ <<create>> AuthenticationController(AuthenticationService authenticationService)
+ ResponseEntity<List<String>> verifyLogin(String username)
+ ResponseEntity<Integer> logout(String username)
+ ResponseEntity<Integer> forgotPassword(ForgotPasswordRequest forgotPasswordRequest)
+ ResponseEntity<Integer> resetPassword(String username, RequestWithPassword requestWithPassword)
+ ResponseEntity<Integer> registerUser(UserDetails user)
+ ResponseEntity<Integer> changePassword(String username, ChangePasswordRequest changePasswordRequest)
+ ResponseEntity<Integer> deleteUser(String username, RequestWithPassword requestWithPassword)
+ }
+
+ class ChangePasswordRequest {
+ <<create>> ChangePasswordRequest(String oldPassword, String newPassword)
+ String getOldPassword()
+ String getNewPassword()
+ }
+
+ class ForgotPasswordRequest {
+ <<create>> ForgotPasswordRequest(String email)
+ String getEmail()
+ }
+
+ class RequestWithPassword {
+ <<create>> ResetPasswordRequest(String password)
+ String getPassword()
+ }
+ }
+}
+
+package model <<Frame>> {
+ class Subscription {
+ <<create>> Subscription(String url, String title)
+ int getID()
+ String getURL()
+ long getLastActionTimestamp()
+ String getTitle()
+ }
+
+ class SubscriptionAction {
+ <<create>> SubscriptionAction(int userID, int subscriptionID)
+ int getID()
+ int getUserID()
+ int getSubscriptionID()
+ long getTimestamp()
+ boolean getAdded()
+ }
+
+ class Episode {
+ <<create>> Episode(int subscriptionID, int id, String url, String title, String thumbnailURL, int total)
+ int getSubscriptionID()
+ int getID()
+ int getGUID()
+ String getURL()
+ String getTitle()
+ int getTotal()
+ }
+
+ enum Action {
+ Download
+ Play
+ Delete
+ New
+ Flattr
+ String getJsonProperty()
+ }
+
+ class EpisodeAction {
+ <<create>> EpisodeAction(Action action, LocalDateTime timestamp, int started, int position)
+ int getEpisodeID()
+ Action getAction()
+ long getTimestamp()
+ int getStarted()
+ int getPosition()
+ void setEpisodeID()
+ EpisodeActionPost getEpisodeActionPost(String podcastURL, String episodeURL)
+ }
+
+ interface UserDetails {
+ String getUsername()
+ String getPassword()
+ Collection<Authority> getAuthorities()
+ boolean isAccountExpired()
+ boolean isAccountLocked()
+ boolean isCredentialsNonExpired()
+ boolean isEnabled()
+ }
+ note left
+ Aus org.springframework.security.core.userdetails.
+ Wird für die Schnittstelle UserDetailsManager benötigt.
+ Stellt wichtige Informationen eines Users bereit.
+ Diese werden nur indirekt von Spring Security
+ benutzt, indem sie vorher in Authentication Objekten
+ gekapselt werden.
+ end note
+
+ class User {
+ --
+ <<create>> User(String username, String password)
+ int getID()
+ String getSessionToken()
+ boolean getEmailIsValidated()
+ .. interface methods ..
+ String getUsername()
+ String getPassword()
+ Collection<Authority> getAuthorities()
+ boolean isAccountExpired()
+ boolean isAccountLocked()
+ boolean isCredentialsNonExpired()
+ boolean isEnabled()
+ }
+
+ interface GrantedAuthority {
+ String getAuthority()
+ }
+ note right
+ Aus org.springframework.security.core.
+ Wird für die Schnittstelle UserDetails benötigt.
+ Repräsentiert eine Autorisierung, die einem
+ Authentication Objekt gewährt wird.
+ end note
+
+ class Authority {
+ <<create>> Authority()
+ String getAuthority()
+ }
+}
+
+package util <<Frame>> {
+ class RSSParser {
+ <<create>> RSSParser(String subscriptionURL)
+ String getSubscriptionTitle()
+ List<Episode> getEpisodes()
+ Episode getEpisodeForURL(String episodeURL)
+ }
+ note bottom
+ Verwendet intern Spring um
+ HTTP-Anfragen zu erstellen.
+ end note
+
+ class CleanCronJob {
+ <<create>> CleanCronJob(JdbcUserDetailsManager jdbcUserDetailsManager)
+ void cleanInvalidUsers()
+ }
+ note bottom
+ Hintergrundservice, der in periodischen Abständen
+ Nutzer, die ihre E-Mail-Adresse nicht nach 24 Stunden
+ bestätigt haben, wieder aus der Datenbank löscht.
+ (Auf die Assoziation zu JdbcUserDetailsManager wird
+ im Sinne der Übersichtlichkeit verzichtet.)
+ end note
+
+ class ResponseEntity<T> {
+ <<create>> ResponseEntity(T body, HttpStatusCode status)
+ T getBody()
+ HttpStatusCode getStatusCode()
+ }
+ note bottom
+ Aus org.springframework.http.
+ Erweitert die Klasse HttpEntity, welche
+ ein HTTP Anfrage- oder Antwort-Objekt
+ repräsentiert, durch einen HttpStatusCode.
+ Wird von den Controller-Methoden als
+ Rückgabewert verwendet.
+ end note
+}
+
+class SecurityConfigurationBasicAuth {
+ <<create>> SecurityConfigurationBasicAuth()
+ PasswordEncoder encoder()
+ UserDetailsManager userDetailsService()
+ SecuryFilterChain fiterChain(HTTPSecurity http) throws Excpetion
+}
+note top
+ Erstellt einen Servlet Filter (springSecurityFilterChain)
+ der für die gesamte Sicherheit zuständig ist (Schutz der URLs,
+ Validierung von Anmeldedaten, Weiterleitung zur Anmeldung, etc.).
+end note
+
+class PSEApplication {
+ <<create>> PSEApplication()
+ void main(String[] args)
+}
+
+database Datenbank
+Datenbank <-[hidden]d- subscriptionsAPI
+Datenbank <-[hidden]d- episodeActionsAPI
+Datenbank <-[hidden]d- authenticationAPI
+() SQL as SQLSub
+() SQL as SQLAuth
+() SQL as SQLEpisode
+
+Datenbank -- SQLSub
+Datenbank -- SQLAuth
+Datenbank -- SQLEpisode
+
+SubscriptionController ..o PSEApplication
+AuthenticationController ..o PSEApplication
+EpisodeActionController ..o PSEApplication
+SecurityConfigurationBasicAuth ..o PSEApplication
+
+PSEApplication --() HTTP
+
+SQLSub )-- SubscriptionDataAccessService: JPA
+' SQLAuth )-- AuthenticationDataAccessService: JPA
+SQLAuth )-- JdbcUserDetailsManager: JDBC
+SQLEpisode )-- EpisodeActionDataAccessService: JPA
+
+Subscription <. SubscriptionAction: ID
+' Subscription <.. SubscriptionDataAccessService: DB
+' SubscriptionAction <.. SubscriptionDataAccessService: DB
+SubscriptionService --o SubscriptionController
+SubscriptionDao <.. SubscriptionService: <<use>>
+Subscription --o SubscriptionTitles
+EpisodeActionPost -o SubscriptionTitles
+SubscriptionDao <|. SubscriptionDataAccessService: <<realize>>
+
+' User <.. AuthenticationDataAccessService: DB
+' User <.. JdbcUserDetailsManager: DB
+UserDetailsManager <.. AuthenticationService: <<use>>
+' AuthenticationDao <.. AuthenticationService: <<use>>
+AuthenticationService --o AuthenticationController
+' AuthenticationDao <|. AuthenticationDataAccessService: <<realize>>
+UserDetailsManager <|. JdbcUserDetailsManager: <<realize>>
+UserDetailsManager <.. SecurityConfigurationBasicAuth: <<use>>
+UserDetails <|.. User: <<realize>>
+User -> Authority
+GrantedAuthority <|.. Authority: <<realize>>
+JavaMailSenderImpl <. AuthenticationService: <<use>>
+
+Action <-- EpisodeAction
+EpisodeActionPost -o EpisodeActionGetResponse
+EpisodeActionPost -o EpisodeActionPostRequest
+EpisodeAction .> Episode: ID
+' EpisodeAction <.. EpisodeActionDataAccessService: DB
+' Episode <.. EpisodeActionDataAccessService: DB
+EpisodeActionDao <.. EpisodeActionService: <<use>>
+EpisodeActionService --o EpisodeActionController
+EpisodeActionDao <|. EpisodeActionDataAccessService: <<realize>>
+
+RSSParser <. SubscriptionDataAccessService: <<use>>
+RSSParser <. EpisodeActionDataAccessService: <<use>>
+' JdbcUserDetailsManager <-- CleanCronJob
+
+model .o Datenbank: ORM (User, SubscriptionAction, Subscription, EpisodeAction, Episode)
+' Datenbank o.. Subscription: ORM
+' Datenbank o.. SubscriptionAction: ORM
+' Datenbank o.. Episode: ORM
+' Datenbank o.. EpisodeAction: ORM
+' Datenbank o.. User: ORM
+
+@enduml
diff --git a/20-implementierungsheft/assets/diagrams/componentdiagram.puml b/20-implementierungsheft/assets/diagrams/componentdiagram.puml
new file mode 100644
index 0000000..7e23754
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/componentdiagram.puml
@@ -0,0 +1,79 @@
+@startuml
+
+[App] as app
+[VueRouter] as router
+
+[EpisodesViewComponent] as episodes_view
+[ForgotPasswordViewComponent] as forgot_password_view
+[LoginViewComponent] as login_view
+[RegistrationViewComponent] as registration_view
+[ResetPasswordViewComponent] as reset_password_view
+[settingsViewComponent] as settings_view
+[SubscriptionsViewComponent] as subscriptions_view
+
+[DashboardLayoutComponent] as dashboard_layout
+[EpisodeComponent] as episode
+[ErrorLogComponent] as error_log
+[FloatingLabelInputComponent] as floating_label_input
+[FormLayoutComponent] as form_layout
+[HelpComponent] as help
+[LastUpdateComponent] as last_update
+[LoadingComponent] as loading
+[NavbarComponent] as navbar
+[PasswordInputComponent] as password_input
+[PasswordValidatorComponent] as password_validator
+[ProgressTimeComponent] as progress_time
+[SubscriptionComponent] as subscription
+
+app --> router
+app --> navbar
+app --> help
+app --> error_log
+
+password_validator --> password_input
+password_input --> floating_label_input
+
+router --> registration_view
+router --> login_view
+router --> reset_password_view
+router --> forgot_password_view
+router --> episodes_view
+router --> subscriptions_view
+router --> settings_view
+
+
+login_view --> form_layout
+login_view --> floating_label_input
+login_view --> password_input
+
+forgot_password_view --> form_layout
+forgot_password_view --> floating_label_input
+
+registration_view --> form_layout
+registration_view --> password_validator
+registration_view --> floating_label_input
+
+reset_password_view --> form_layout
+reset_password_view --> password_validator
+
+settings_view --> dashboard_layout
+settings_view --> floating_label_input
+settings_view --> password_input
+settings_view --> password_validator
+
+episodes_view --> dashboard_layout
+episodes_view --> episode
+episodes_view --> loading
+
+episode --> last_update
+episode --> progress_time
+
+subscriptions_view --> dashboard_layout
+subscriptions_view --> floating_label_input
+subscriptions_view --> loading
+subscriptions_view --> subscription
+
+subscription --> last_update
+subscription --> progress_time
+
+@enduml
diff --git a/20-implementierungsheft/assets/diagrams/db.puml b/20-implementierungsheft/assets/diagrams/db.puml
new file mode 100644
index 0000000..bdefaea
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/db.puml
@@ -0,0 +1,78 @@
+@startuml
+' Type Symbol
+' Zero or One |o--
+' Exactly One ||--
+' Zero or Many }o--
+' One or Many }|--
+
+skinparam linetype ortho
+
+entity User {
+ * int id <<unique>>
+ * <u>String email</u>
+ * String password
+ * boolean verified
+ * long created_at
+}
+
+entity SubscriptionAction {
+ * int id <<unique>>
+ * <u>int user_id</u>
+ * long timestamp
+ * int subscription_id
+ * boolean added
+}
+
+entity Subscription {
+ * int id <<unique>>
+ * <u>String url</u>
+ * long timestamp
+ * String title
+}
+
+entity Episode {
+ * int id <<unique>>
+ * <u>int guid <<unique>></u>
+ * <u>String url</u>
+ * String title
+ * int total
+ * int subscription_id
+}
+note right
+ Wenn der Client eine GUID aus dem Feed mitsendet, wird
+ diese statt der URL verwendet um die Episode zu finden.
+ So wird die Episode auch noch gefunden, nachdem sich
+ die URL geändert hat.
+end note
+note bottom of Episode
+ Wenn für die Episoden-URL einer EpisodeAction noch keine Episode in der Datenbank steht,
+ dann schreibe dafür ein Dummy-Objekt in Datenbank und lade asynchron die Episoden der Subscription.
+ Ersetze dann die Dummy-Objekte durch die Episoden und setze den Timestamp der Subscription auf
+ die aktuelle Zeit.
+ Um DoS-Angriffe auf den Backend-Server abzuwenden, können die Episoden einer Subscription erst
+ nach einer Stunde erneut gefetched werden. Bis dahin werden für EpisodeActions, die sich auf noch
+ nicht geladene Episoden beziehen, nur Dummy-Objekte für die Episoden in die Datenbank geschrieben.
+ Es sei noch darauf hingewiesen, dass diese Dummy-Episoden bei Anfragen nicht mit ausgegeben werden.
+end note
+
+entity EpisodeAction {
+ * int id <<unique>>
+ * <u>int user_id</u>
+ * int episode_id
+ * long timestamp
+ * int action
+ * int started
+ * int position
+}
+note right
+ Speichere für jede Episode
+ nur letzte Play-Action.
+endnote
+
+User ||--o{ EpisodeAction
+User ||--o{ SubscriptionAction
+SubscriptionAction }|--|| Subscription
+EpisodeAction }|--|| Episode
+Subscription ||-right-|{ Episode
+
+@enduml
diff --git a/20-implementierungsheft/assets/diagrams/deployment.puml b/20-implementierungsheft/assets/diagrams/deployment.puml
new file mode 100644
index 0000000..26918e2
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/deployment.puml
@@ -0,0 +1,59 @@
+@startuml
+
+node "<<device>> \nBackend Server" as backendServer{
+ database " <<database system>> \n MariaDB Server 10.6" as database {
+ rectangle rectangle1 [
+ <<schema>>
+ User
+ ]
+ rectangle rectangle2 [
+ <<schema>>
+ SubscriptionAction
+ ]
+ rectangle rectangle3 [
+ <<schema>>
+ EpisodeAction
+ ]
+ rectangle rectangle4 [
+ <<schema>>
+ Subscription
+ ]
+ rectangle rectangle5 [
+ <<schema>>
+ Episode
+ ]
+ }
+
+ node "<<framework>> \nJava Spring" as javaSpring{
+ node " <<device>> \n Tomcat Webserver"
+ }
+}
+
+node "<<device>> \nFrontend" as frontendServer {
+
+}
+
+node "<<device>> \nEndgerät" as terminal {
+ node "<<application>> \nBrowser" as browser
+ node "<<application>> \nPodcatcher" as podcatcher
+}
+
+backendServer "1" - "*" podcatcher
+
+node "<<device>> \nFrontend Server" as frontendServer{
+ node "<<framework>> \nVue.js" as vuejs {
+
+ }
+}
+
+podcatcher -[hidden] browser
+
+backendServer - "1" frontendServer
+
+database "1" -- "1" javaSpring
+
+browser "*" -- frontendServer
+
+
+
+@enduml
diff --git a/20-implementierungsheft/assets/diagrams/gantt-plan.puml b/20-implementierungsheft/assets/diagrams/gantt-plan.puml
new file mode 100644
index 0000000..0e90aa2
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/gantt-plan.puml
@@ -0,0 +1,31 @@
+@startgantt
+
+printscale daily zoom 5
+project starts on 2023-01-30
+
+-- Backend --
+[Controller-Schicht] on {Immanuel} lasts 2 days
+[Service-Schicht (Daten durchreichen)] on {Daniel} lasts 2 days
+[Authentifizierung] on {Gero} lasts 4 days
+[Model-Paket] on {Daniel} lasts 1 days
+[Datenbank aufsetzen] on {Immanuel} lasts 4 days
+[Util-Paket (RSSParser)] on {Daniel} {Lukas} lasts 6 days
+[DataAccess-Schicht] on {Immanuel} {Julius} lasts 8 days
+[Service-Schicht (Geschäftslogik)] on {Daniel} {Immanuel} lasts 8 days
+[Util-Paket (CleanCronJob)] on {Julius} lasts 2 days
+-- Frontend --
+[Komponenten] on {Gero} {Julius} {Lukas} lasts 15 days
+[API-Anbindung] on {Gero} {Lukas} lasts 4 days
+
+'Backend
+[Service-Schicht (Daten durchreichen)] starts at [Controller-Schicht]'s end
+[Datenbank aufsetzen] starts at [Model-Paket]'s end
+[Authentifizierung] starts at [Controller-Schicht]'s end
+[DataAccess-Schicht] starts at [Datenbank aufsetzen]'s end
+[Util-Paket (RSSParser)] starts at [Datenbank aufsetzen]'s end
+[Service-Schicht (Geschäftslogik)] starts at [DataAccess-Schicht]'s end
+[Util-Paket (CleanCronJob)] starts at [DataAccess-Schicht]'s end
+'Frontend
+[API-Anbindung] starts at [DataAccess-Schicht]'s end
+
+@endgantt \ No newline at end of file
diff --git a/20-implementierungsheft/assets/diagrams/gantt-reality.puml b/20-implementierungsheft/assets/diagrams/gantt-reality.puml
new file mode 100644
index 0000000..f726c56
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/gantt-reality.puml
@@ -0,0 +1,39 @@
+@startgantt
+
+printscale daily zoom 5
+project starts on 2023-01-30
+
+-- Backend --
+[Controller-Schicht] on {Immanuel} lasts 3 days
+[Model-Paket] on {Daniel} lasts 3 days
+[Datenbank aufsetzen] on {Immanuel} lasts 6 days
+[Util-Paket (RSSParser)] on {Daniel} {Lukas} lasts 32 days
+[DAO-Interfaces] on {Julius} {Immanuel} lasts 6 days
+[Authentifizierung] on {Immanuel} lasts 13 days
+[Service-Schicht mit Datenzugriff] on {Julius} lasts 14 days
+[Util-Paket (CleanCronJob)] on {Daniel} lasts 2 days
+[Docker] on {Daniel} lasts 12 days
+[EMailService] on {Gero} lasts 1 days
+-- Frontend --
+[Komponenten] on {Gero} {Julius} lasts 15 days
+[Mehrsprachigkeit] on {Lukas} lasts 5 days
+[Router] on {Gero} lasts 1 days
+[API-Anbindung] on {Gero} {Lukas} lasts 28 days
+[Error-Handling] on {Gero} lasts 5 days
+
+'Backend
+[Datenbank aufsetzen] starts at [Model-Paket]'s end
+[Util-Paket (RSSParser)] starts at [Datenbank aufsetzen]'s end
+[DAO-Interfaces] starts at [Datenbank aufsetzen]'s end
+[Authentifizierung] starts at [DAO-Interfaces]'s end
+[Service-Schicht mit Datenzugriff] starts at [DAO-Interfaces]'s end
+[Util-Paket (CleanCronJob)] starts at [DAO-Interfaces]'s end
+[Docker] starts at [Util-Paket (CleanCronJob)]'s end
+[EMailService] starts 2023-02-14
+'Frontend
+[Mehrsprachigkeit] starts 2023-02-01
+[Router] starts at [Mehrsprachigkeit]'s end
+[API-Anbindung] starts at [Router]'s end
+[Error-Handling] starts 2023-02-05
+
+@endgantt \ No newline at end of file
diff --git a/20-implementierungsheft/assets/diagrams/sequencediagram-forgotAndResetPW.puml b/20-implementierungsheft/assets/diagrams/sequencediagram-forgotAndResetPW.puml
new file mode 100644
index 0000000..603130c
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/sequencediagram-forgotAndResetPW.puml
@@ -0,0 +1,41 @@
+@startuml
+
+skinparam ParticipantPadding 30
+
+participant AuthenticationController << (C, #ADD1B2) @Controller >>
+-> AuthenticationController: ""POST /api/2/auth/forgot.json"" \n//@RequestBody ForgotPasswordRequest forgotPasswordRequest// \n\n-> forgotPassword(//forgotPasswordRequest//)
+activate AuthenticationController
+participant AuthenticationService << (C, #ADD1B2) @Service >>
+AuthenticationController -> AuthenticationService: forgotPassword(//forgotPasswordRequest//)
+activate AuthenticationService
+participant JavaMailSenderImpl << (C, #ADD1B2) >>
+AuthenticationService -> JavaMailSenderImpl: create link to reset password with JWT as URL parameter \n-> send(SimpleMailMessage simpleMessage) with link
+activate JavaMailSenderImpl
+<<- JavaMailSenderImpl: sends email with link containing a JWT to reset password
+JavaMailSenderImpl --> AuthenticationService
+deactivate JavaMailSenderImpl
+AuthenticationService --> AuthenticationController: int indicating status
+deactivate AuthenticationService
+<-- AuthenticationController: ResponseEntity<Integer> indicating status \n\n-> ""HTTP status code""
+deactivate AuthenticationController
+||60||
+-> AuthenticationController: ""PUT /api/2/auth/{username}/resetpassword.json"" \n//@RequestParam String jwt// \n//@RequestBody ResetPasswordRequest resetPasswordRequest// \n\n-> login user (""username"") via JWT (//jwt//) \n-> resetPassword(""username"", //resetPasswordRequest//)
+activate AuthenticationController
+AuthenticationController -> AuthenticationService: resetPassword(""username"", //resetPasswordRequest//)
+activate AuthenticationService
+participant JdbcUserDetailsManager << (C, #ADD1B2) @Repository >>
+AuthenticationService -> JdbcUserDetailsManager: String oldPassword = //resetPasswordRequest//.getOldPassword() \nString newPassword = //resetPasswordRequest//.getNewPassword() \n-> changePassword(newPassword, oldPassword)
+activate JdbcUserDetailsManager
+database Database
+JdbcUserDetailsManager -> Database: change password of logged in user
+activate Database
+Database --> JdbcUserDetailsManager
+deactivate Database
+JdbcUserDetailsManager --> AuthenticationService: int indicating status
+deactivate JdbcUserDetailsManager
+AuthenticationService --> AuthenticationController: int indicating status
+deactivate AuthenticationService
+<-- AuthenticationController: ResponseEntity<Integer> indicating status \n\n-> ""HTTP status code""
+deactivate AuthenticationController
+
+@enduml \ No newline at end of file
diff --git a/20-implementierungsheft/assets/diagrams/sequencediagram-getEpisodeActions.puml b/20-implementierungsheft/assets/diagrams/sequencediagram-getEpisodeActions.puml
new file mode 100644
index 0000000..47497d5
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/sequencediagram-getEpisodeActions.puml
@@ -0,0 +1,38 @@
+@startuml
+
+' title =**Get All Episode Actions**
+
+participant EpisodeActionController << (C, #ADD1B2) @Controller >>
+-> EpisodeActionController: ""GET /api/2/episodes/{username}.json"" \n//@RequestParam("device") String deviceID// \n//@RequestParam("aggregated") boolean aggregated// \n\n-> getEpisodeActions(""username"", //deviceID//, //aggregated//)
+note right
+ Die Parameter //deviceID// und //aggregated// werden ignoriert,
+ da nicht zwischen Geräten unterschieden und für jede
+ Episode sowieso nur die letzte Play-Action gespeichert
+ wird. Dies gilt für alle GET-Anfragen der Episode Actions API.
+end note
+activate EpisodeActionController
+participant EpisodeActionService << (C, #ADD1B2) @Service >>
+EpisodeActionController -> EpisodeActionService: getEpisodeActions(""username"")
+activate EpisodeActionService
+participant EpisodeActionDataAccessService << (C, #ADD1B2) @Repository >>
+EpisodeActionService -> EpisodeActionDataAccessService: getEpisodeActions(""username"")
+activate EpisodeActionDataAccessService
+EpisodeActionDataAccessService -> EpisodeActionDataAccessService: getEpisodeActionsSince(""username"", \nLocalDateTime.MIN.toEpochSecond(ZoneOffset.UTC))
+database Database
+activate EpisodeActionDataAccessService
+EpisodeActionDataAccessService -> Database: get all EpisodeActions for all subscribed podcasts
+activate Database
+Database --> EpisodeActionDataAccessService: List<EpisodeAction> selectedEpisodeActions \n-> then remove all older than LocalDateTime.MIN (none)
+EpisodeActionDataAccessService -> Database: join EpisodeActions in selectedEpisodeActions with episodeURL of Episode
+Database --> EpisodeActionDataAccessService
+deactivate Database
+EpisodeActionDataAccessService --> EpisodeActionDataAccessService: List<EpisodeActionPost> episodeActionPosts
+deactivate EpisodeActionDataAccessService
+EpisodeActionDataAccessService --> EpisodeActionService: List<EpisodeActionPost> episodeActionPosts
+deactivate EpisodeActionDataAccessService
+EpisodeActionService --> EpisodeActionController: List<EpisodeActionPost> episodeActionPosts
+deactivate EpisodeActionService
+<-- EpisodeActionController: ResponseEntity<EpisodeActionGetResponse> response \n\n-> ""HTTP status code"" \n-> ""JSON""
+deactivate EpisodeActionController
+
+@enduml \ No newline at end of file
diff --git a/20-implementierungsheft/assets/diagrams/sequencediagram-getEpisodeActionsOfPodcastSince.puml b/20-implementierungsheft/assets/diagrams/sequencediagram-getEpisodeActionsOfPodcastSince.puml
new file mode 100644
index 0000000..d8797d1
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/sequencediagram-getEpisodeActionsOfPodcastSince.puml
@@ -0,0 +1,32 @@
+@startuml
+
+' title =**Get Episode Actions of Podcast Since**
+
+participant EpisodeActionController << (C, #ADD1B2) @Controller >>
+-> EpisodeActionController: ""GET /api/2/episodes/{username}.json"" \n//@RequestParam("podcast") String podcastURL// \n//@RequestParam("device") String deviceID// \n//@RequestParam("since") long since// \n//@RequestParam("aggregated") boolean aggregated// \n\n-> getEpisodeActionsOfPodcastSince(""username"", //podcastURL//, //deviceID//, //since//, //aggregated//)
+note right
+ Die Parameter //deviceID// und //aggregated// werden ignoriert.
+ Siehe Notiz in Sequenzdiagramm **Get All Episode Actions**.
+end note
+activate EpisodeActionController
+participant EpisodeActionService << (C, #ADD1B2) @Service >>
+EpisodeActionController -> EpisodeActionService: getEpisodeActionsOfPodcastSince(""username"", //podcastURL//, //since//)
+activate EpisodeActionService
+participant EpisodeActionDataAccessService << (C, #ADD1B2) @Repository >>
+EpisodeActionService -> EpisodeActionDataAccessService: getEpisodeActionsOfPodcastSince(""username"", //podcastURL//, //since//)
+activate EpisodeActionDataAccessService
+database Database
+EpisodeActionDataAccessService -> Database: get all EpisodeActions the given podcast (//podcastURL//)
+activate Database
+Database --> EpisodeActionDataAccessService: List<EpisodeAction> selectedEpisodeActions \n-> then remove all older than //since//
+EpisodeActionDataAccessService -> Database: join EpisodeActions in selectedEpisodeActions with episodeURL of Episode
+Database --> EpisodeActionDataAccessService
+deactivate Database
+EpisodeActionDataAccessService --> EpisodeActionService: List<EpisodeActionPost> episodeActionPosts
+deactivate EpisodeActionDataAccessService
+EpisodeActionService --> EpisodeActionController: List<EpisodeActionPost> episodeActionPosts
+deactivate EpisodeActionService
+<-- EpisodeActionController: ResponseEntity<EpisodeActionGetResponse> response \n\n-> ""HTTP status code"" \n-> ""JSON""
+deactivate EpisodeActionController
+
+@enduml \ No newline at end of file
diff --git a/20-implementierungsheft/assets/diagrams/sequencediagram-getSubscriptions.puml b/20-implementierungsheft/assets/diagrams/sequencediagram-getSubscriptions.puml
new file mode 100644
index 0000000..4d8ab90
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/sequencediagram-getSubscriptions.puml
@@ -0,0 +1,38 @@
+@startuml
+
+' title =**Get All Subscriptions**
+
+participant SubscriptionController << (C, #ADD1B2) @Controller >>
+-> SubscriptionController: ""GET /subscriptions/{username}.json"" \n"" /subscriptions/{username}/{deviceid}.json"" \n//@RequestParam("jsonp") String functionJSONP// \n\n-> getSubscriptions(""username"", ""deviceid"", //functionJSONP//)
+activate SubscriptionController
+note right
+ Die Parameter ""deviceid"" und
+ //functionJSONP// werden ignoriert,
+ da nicht zwischen Geräten unterschieden
+ und JSONP nicht unterstützt wird.
+end note
+participant SubscriptionService << (C, #ADD1B2) @Service >>
+SubscriptionController -> SubscriptionService: getSubscriptions(""username"")
+activate SubscriptionService
+participant SubscriptionDataAccessService << (C, #ADD1B2) @Repository >>
+SubscriptionService -> SubscriptionDataAccessService: getSubscriptions(""username"")
+activate SubscriptionDataAccessService
+SubscriptionDataAccessService -> SubscriptionDataAccessService: getSubscriptionsSince(""username"", LocalDateTime.MIN)
+database Database
+activate SubscriptionDataAccessService
+SubscriptionDataAccessService -> Database: get all Subscriptions for ""username""
+activate Database
+Database --> SubscriptionDataAccessService: List<Subscription> subscriptions
+SubscriptionDataAccessService -> Database: get Podcasts from Subscriptions
+Database --> SubscriptionDataAccessService: List<Podcast> subscribedPodcasts
+deactivate Database
+SubscriptionDataAccessService --> SubscriptionDataAccessService: List<String> podcastURLs
+deactivate SubscriptionDataAccessService
+SubscriptionDataAccessService --> SubscriptionService: List<String> podcastURLs
+deactivate SubscriptionDataAccessService
+SubscriptionService --> SubscriptionController: List<String> podcastURLs
+deactivate SubscriptionService
+<-- SubscriptionController: ResponseEntity<List<String>> podcastURLs \n \n-> ""HTTP status code"" \n-> ""JSON""
+deactivate SubscriptionController
+
+@enduml \ No newline at end of file
diff --git a/20-implementierungsheft/assets/diagrams/sequencediagram-register.puml b/20-implementierungsheft/assets/diagrams/sequencediagram-register.puml
new file mode 100644
index 0000000..b7b7aa1
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/sequencediagram-register.puml
@@ -0,0 +1,26 @@
+@startuml
+
+' title =**Register**
+
+participant AuthenticationController << (C, #ADD1B2) @Controller >>
+-> AuthenticationController: ""POST /api/2/auth/register.json"" \n//@RequestBody UserDetails user// \n\n-> registerUser(//user//)
+activate AuthenticationController
+participant AuthenticationService << (C, #ADD1B2) @Service >>
+AuthenticationController -> AuthenticationService: registerUser(//user//)
+activate AuthenticationService
+participant JdbcUserDetailsManager << (C, #ADD1B2) @Repository >>
+AuthenticationService -> JdbcUserDetailsManager: createUser(//user//)
+activate JdbcUserDetailsManager
+database Database
+JdbcUserDetailsManager -> Database: create new User with given UserDetails (//user//)
+activate Database
+Database --> JdbcUserDetailsManager
+deactivate Database
+JdbcUserDetailsManager --> AuthenticationService: int indicating status
+deactivate JdbcUserDetailsManager
+AuthenticationService --> AuthenticationController: int indicating status
+deactivate AuthenticationService
+<-- AuthenticationController: ResponseEntity<Integer> indicating status \n\n-> ""HTTP status code""
+deactivate AuthenticationController
+
+@enduml \ No newline at end of file
diff --git a/20-implementierungsheft/assets/diagrams/sequencediagram-uploadEpisodeActions.puml b/20-implementierungsheft/assets/diagrams/sequencediagram-uploadEpisodeActions.puml
new file mode 100644
index 0000000..d3dac57
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/sequencediagram-uploadEpisodeActions.puml
@@ -0,0 +1,38 @@
+@startuml
+
+' title =**Upload Episode Actions**
+
+participant EpisodeActionController << (C, #ADD1B2) @Controller >>
+-> EpisodeActionController: ""POST /api/2/episodes/{username}.json"" \n//@RequestBody EpisodeActionPostRequest episodeActionPostRequest// \n\n-> addEpisodeActions(""username"", //episodeActionPostRequest//)
+activate EpisodeActionController
+participant EpisodeActionService << (C, #ADD1B2) @Service >>
+EpisodeActionController -> EpisodeActionService: addEpisodeActions(""username"", \nepisodeActionPosts = //episodeActionPostRequest//.getEpisodeActionPosts())
+activate EpisodeActionService
+participant EpisodeActionDataAccessService << (C, #ADD1B2) @Repository >>
+EpisodeActionService -> EpisodeActionDataAccessService: addEpisodeActions(""username"", episodeActionPosts)
+database Database
+activate EpisodeActionDataAccessService
+loop for each EpisodeActionPost in episodeActionPosts -> episodeAction = episodeActionPost.getEpisodeAction()
+opt episodeAction.getAction().equals(Action.PLAY)
+EpisodeActionDataAccessService -> Database: set episodeID field of episodeAction for this ""username"" via podcastURL and episodeURL
+activate Database
+Database --> EpisodeActionDataAccessService
+EpisodeActionDataAccessService -> Database: get last EpisodeAction with this episodeID if present
+Database --> EpisodeActionDataAccessService: Optional<EpisodeAction> lastEpisodeAction
+opt lastEpisodeAction.isPresent()
+EpisodeActionDataAccessService -> Database: replace lastEpisodeAction with episodeAction
+else else
+EpisodeActionDataAccessService -> Database: add episodeAction to DB as new entry
+end
+Database --> EpisodeActionDataAccessService
+deactivate Database
+end
+end
+EpisodeActionDataAccessService --> EpisodeActionService: long latestTimestamp
+deactivate EpisodeActionDataAccessService
+EpisodeActionService --> EpisodeActionController: LocalDateTime timestamp = LocalDateTime.ofEpochSecond(latestTimestamp, 0, ZoneOffset.UTC)
+deactivate EpisodeActionService
+<-- EpisodeActionController: ResponseEntity<EpisodeActionPostResponse> \n(with empty list for updateURLs) \n\n-> ""HTTP status code"" \n-> ""JSON""
+deactivate EpisodeActionController
+
+@enduml \ No newline at end of file
diff --git a/20-implementierungsheft/assets/diagrams/sequencediagram-uploadSubscriptions.puml b/20-implementierungsheft/assets/diagrams/sequencediagram-uploadSubscriptions.puml
new file mode 100644
index 0000000..1edc8cf
--- /dev/null
+++ b/20-implementierungsheft/assets/diagrams/sequencediagram-uploadSubscriptions.puml
@@ -0,0 +1,32 @@
+@startuml
+
+' title =**Upload Subscriptions**
+
+participant SubscriptionController << (C, #ADD1B2) @Controller >>
+-> SubscriptionController: ""PUT /subscriptions/{username}/{deviceid}.json"" \n//@RequestBody List<String> subscriptions// \n\n-> uploadSubscriptions(""username"", ""deviceid"", //subscriptions//)
+activate SubscriptionController
+participant SubscriptionService << (C, #ADD1B2) @Service >>
+SubscriptionController -> SubscriptionService: uploadSubscriptions(""username"", //subscriptions//)
+activate SubscriptionService
+participant SubscriptionDataAccessService << (C, #ADD1B2) @Repository >>
+SubscriptionService -> SubscriptionDataAccessService: uploadSubscriptions(""username"", //subscriptions//)
+activate SubscriptionDataAccessService
+database Database
+SubscriptionDataAccessService -> Database: delete all subsciptions of ""username""
+activate Database
+Database --> SubscriptionDataAccessService
+SubscriptionDataAccessService -> SubscriptionDataAccessService: addSubscriptions(""username"", //subscriptions//)
+activate SubscriptionDataAccessService
+SubscriptionDataAccessService -> Database: upload all subscriptions (//subscriptions//) for ""username""
+Database --> SubscriptionDataAccessService
+deactivate Database
+SubscriptionDataAccessService --> SubscriptionDataAccessService: int indicating status
+deactivate SubscriptionDataAccessService
+SubscriptionDataAccessService --> SubscriptionService: int indicating status
+deactivate SubscriptionDataAccessService
+SubscriptionService --> SubscriptionController: int indicating status
+deactivate SubscriptionService
+<-- SubscriptionController: ResponseEntity<String> with empty String for success \n\n-> ""HTTP status code"" \n-> ""JSON""
+deactivate SubscriptionController
+
+@enduml \ No newline at end of file