diff options
author | Orangerot <purple@orangerot.dev> | 2024-05-24 17:42:08 +0200 |
---|---|---|
committer | Orangerot <purple@orangerot.dev> | 2024-05-24 17:47:22 +0200 |
commit | 7fcdc1c788725f866de71fc9dfd8c4d1cb132b57 (patch) | |
tree | 89931c85ae3f149884ba02c69862558e93f01531 /20-implementierungsheft/assets/diagrams |
Diffstat (limited to '20-implementierungsheft/assets/diagrams')
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 |