Zum Inhalt

Struktur #

Das Projekt beinhaltet vier Hauptkomponenten, die Spring Boot API, Datenbank und Platform API's. Die API verfügt über eine REST und GraphQL-Schnittstelle, welche von Instagram oder Twitter einen Benutzer abfragen. Die Resultate der Abfragen werden dann in der Datenbank gespeichert.

Komponenten

@startuml "Komponenten"

node "API" {
    [GraphQL]
    folder Platformen {
        [Instagram]
        [Twitter]
    }
    GraphQL - Platformen
}

API -up- [Instagram API]
API -up- [Twitter API]

API -down- HTTPS

cloud "Hostinger" {
    database "Maria DB" {
        [Entities]
    }
}
HTTPS - Hostinger

@enduml

Wie in den Zielen beschrieben beinhaltet diese API kein Frontend, sondern stellt nur Endpunkte zur Verfügung, die von anderen APIs aufgerufen werden können.

Übersicht#

Das Projekt ist so aufgebaut, dass hinter jedem Controller ein Service steht. Die Services beinhalten die Logik, welche in den Controllers aufgerufen werden. So ist die Logik nicht an einen Controller gebunden und kann auch in anderen Projekten oder Klassen verwendet werden.

Um eine gute Übersicht über alle Klassen zu bekommen wurde ein Klassendiagramm mit allen Verlinkungen erstellt:

Klassendiagramm

@startuml "Klassendiagramm"

component SpringBoot {
    package Auth {
        class AuthAccount <<(R,orchid)>> {
            - apiKey : final String
            - email : final String
            - password : final String
        }

        class AuthController {
            - authService : AuthService
            ..
            + generateKey(AuthAccount authAccount) : ResponseHandler<String>
            + regenerateKey(AuthAccount authAccount) : ResponseHandler<String>
            + deleteKey(String apiKey) : ResponseHandler<String>
        }

        class AuthService {
            - accRepository : AccountRepository
            + createAccount(AuthAccount authAccount) : ResponseHandler<String>
            + regenerateAPIKey(AuthAccount authAccount) : ResponseHandler<String>
            + deleteAccount(String apiKey) : boolean
        }

        class Credentials {
            - Credentials()
            + {static} generateApiKey(AuthAccount account) : String
            + {static} validateApiKey(String apiKey) : void
            + {static} validateCredentials(AuthAccount auth) : void
            + {static} extractLimit(Optional<Integer> limit) : int
            + {static} containsNumber(String value) : boolean
            + {static} isValidUsername(String username) : boolean
        }

        AuthController --> AuthService
    }

    package Base {
        abstract BaseController {
            # service : final BaseService
            ..
            # BaseController(BaseService service)
            + status() : ResponseHandler<String>
            + getuser(String username, String apiKey) : ResponseHandler<User>
            + getPosts(String username, String apiKey, int limit) : ResponseHandler<Set<Post>>
        }

        abstract BaseService {
            - platform : final Platform
            - rateLimiter : final RateLimiter
            # webClient : final WebClient
            # accountRepository : AccountRepository
            # userRepository : UserRepository
            # postRepository : PostRepository
            ..
            # BaseService(Platform platform)
            # BaseService(Platform platform, WebClient webClient)
            + {static} getBaseWebClient() : Builder
            + getUser(String apiKey, String username) : ResponseHandler<User>
            + getPosts(String apiKey, String username, int limit) : ResponseHandler<Set<Post>>
            + getJsonData(String requestUri, String apiKey, String username) : ResponseHandler<String>
            - {static} handleResponse(long accountId, ClientResponse response, Platform platform) : Mono<String>
            .. abstract ..
            + {abstract} mapJsonToUser(String json) : User
            + {abstract} mapJsonToPosts(String json, int limit) : Set<Post>
            # {abstract} getUserRequestUri(String username) : String
            # {abstract} getPostsRequestUri(String username, int limit) : String
        }

        BaseController --> BaseService
    }

    frame Platforms {
        package Instagram {
            class InstagramController extends BaseController{
                + InstagramController(InstagramService service)
                + status() : ResponseHandler<String>
                + getUser(String username, String apiKey) : ResponseHandler<User>
                + getPosts(String username, String apiKey, int limit) : ResponseHandler<Set<Post>>
            }

            class InstagramService extends BaseService {
                + InstagramService()
                - {static} extractLinkedUsers(JsonNode jsonNode, String description) : Set<String>
                - {static} extractCommentsCount(JsonNode jsonNode) : int
                - {static} extractAiPrediction(JsonNode jsonNode) : String
                - {static} extractMedia(JsonNode mediaNode) : Media
                .. Overrides ..
                + mapJsonToUser(String json) : User
                + mapJsonToPosts(String json, int limit) : Set<Post>
                # getUserRequestUri(String username) : String
                # getPostsRequestUri(String username, int limit) : String
            }

            InstagramController --> InstagramService
        }

        package Twitter {
            class TwitterController extends BaseController{
                + TwitterController(TwitterService service)
                + status() : ResponseHandler<String>
                + getUser(String username, String apiKey) : ResponseHandler<User>
                + getPosts(String username, String apiKey, int limit) : ResponseHandler<Set<Post>>
            }

            class TwitterService extends BaseService {
                + TwitterService(String bearer)
                - {static} extractLinkedUsers(JsonNode entities) : Set<String>
                - {static} extractAiPrediction(JsonNode jsonNode) : String
                - {static} extractMedias(JsonNode includeNode) : Map<String, Media>
                - {static} addMediasToPost(Map<String, Media> mediaMap, JsonNode node, Post post, String username) : void
                .. Overrides ..
                + mapJsonToUser(String json) : User
                + mapJsonToPosts(String json, int limit) : Set<Post>
                # getUserRequestUri(String username) : String
                # getPostsRequestUri(String username, int limit) : String
            }

            TwitterController --> TwitterService
        }

        package GraphQL {
            class GraphQLEndpoint {
                - instagramService : InstagramService
                - twitterService : TwitterService
                ..
                + user(String apiKey, String username, Platform platform) : User
                + posts(DgsDataFetchingEnvironment dfe, Optional<Integer> limit) : Set<Post>
                - handleResponse(ResponseHandler<T> responseHandler) : T
                - {static} getParentArgument(DgsDataFetchingEnvironment dfe, String name) : String
            }

            GraphQLEndpoint --> InstagramService
            GraphQLEndpoint --> TwitterService
        }
    }

    package Utils {
        class Helper {
            - Helper()
            + {static} trimToSize(String value, int size) : String
            + {static} toUpperCaseNullable(String value) : String
            + {static} equalsIgnoreCase(String value, String other) : boolean
        }

        class RateLimiter << (S,#FF7700) Singleton>> {
            - {static} LIMIT : final int = 900
            - {static} rateLimiterMap : HashBiMap<Platform, RateLimiter>
            - {static} bucketMap : final ConcurrentMap<Long, Bucket>
            ..
            - RateLimiter()
            + {static} synchronized getInstance(Platform platform) : RateLimiter
            - {static} createBucket() : Bucket
            + tryConsume(long accountId) : boolean
            ~ {static} resetRateLimiter() : void
        }

        class ResponseHandler<<T>> {
            - data : final T
            - message : final String
            - status : final HttpStatus
            ..
            + ResponseHandler(String message, HttpStatus status)
            + ResponseHandler(T data, String message, HttpStatus status)
            + generateResponse() : ResponseEntity<Object>
            + {static} generateResponse(String message, HttpStatus status) : ResponseEntity<Object>
            + {static} generateResponse(Object data, String message, HttpStatus status) : ResponseEntity<Object>
        }

        RateLimiter --> RateLimiter : Singleton
        BaseService --> RateLimiter
    }
}

database Database {
    package Authentication {
        class Account {
            - id : Long
            - {static} createdAt : final Instant
            - apiKey : String
            - cred : String
            ..
            # Account()
            + Account(String apiKey, AuthAccount account)
        }
    }

    package Entities {
        class User {
            - id : Long
            - cachedSince : Instant
            - platform : Platform
            - username  : String
            - name : String
            - bio : String
            - website : String
            - profilePictureUrl : String
            - followerCount : int
            - followingCount : int
            - isVerified : boolean
            - isPrivate : boolean
            - posts : Set<Post>
            ..
            # User()
            + User( All Arguments )
            + isValidCache() : boolean
            + setUsername(String username) : void
            + setName(String name) : void
            + setBio(String bio) : void
            + setWebsite(String website) : void
            + setProfilePictureUrl(String profilePictureUrl) : void
            + setPosts(Set<Post> posts) : void
            + getPosts() : Set<Post>
            + addPost(Post post) : void
        }

        class Post {
            - id : Long
            - author : User
            - description : String
            - publishedAt : Instant
            - likesCount : int
            - commentsCount : int
            - medias : Set<Media>
            - linkedUsers : Set<String>
            - aiPrediction : String
            ..
            # Post()
            + Post(All Arguments)
            + setAuthor(User author) : void
            + setDescription(String description) : void
            + setAiPrediction(String aiPrediction) : void
            .. Media ..
            + addMedia(Media media) : void
            + removeMedia(Media media) : void
            + hasMedia() : boolean
        }

        class Media {
            - id : Long
            - type : MediaType
            - url : String
            - post : Post
            ..
            # Media()
            + Media(MediaType type, String url)
            + setUrl(String url) : void
            + setPost(Post post) : void
        }

        enum MediaType {
            UNKNOWN
            TEXT
            IMAGE
            VIDEO
            GIF
        }

        enum Platform {
            TWITTER
            INSTAGRAM
        }

        User "1" --o "*" Platform
        User "1" o--o "0..*" Post
        Post "1" o--o "1..*" Media
        Media "1" --o "0..*" MediaType
        BaseService --o Platform
    }

    package Repositories {
        interface AccountRepository<Account> extends JpaRepository {
            + findByApiKey(String apiKey) : Optional<Account>
            + deleteByApiKey(String apiKey) : void
            + existsByCredStartsWith(String cred) : boolean
        }

        interface UserRepository<User> extends JpaRepository {
            + findByUsernameAndPlatform(String username, Platform platform) : Optional<User>
        }

        interface PostRepository<Post> extends JpaRepository {
            + findByAuthor(User author) : Set<Post>
        }

        BaseService --> AccountRepository
        BaseService --> UserRepository
        BaseService --> PostRepository
        AuthService --> AccountRepository
    }
}

@enduml

Für bessere Lesbarkeit sind die einzelnen Teile in Unterkapiteln unterteilt. Der Datenbankbereich befindet sich unter Datenbank.

Auth#

Für die Authentifizierung und Erstellung von API-Keys werden die Auth Klassen verwendet. Die Klasse Credentials beinhaltet Validierung- und Hashmethoden. AuthAccount wird verwendet um die Anfragen anzunehmen und in einen richtigen Account umzuwandeln. Der Service und Controller sind zuständig für die Authentifizierung und Verwaltung der API-Keys.

Auth

@startuml "Auth"

package Auth {
    class AuthAccount <<(R,orchid)>> {
        - apiKey : final String
        - email : final String
        - password : final String
    }
     class AuthController {
        - authService : AuthService
        ..
        + generateKey(AuthAccount authAccount) : ResponseHandler<String>
        + regenerateKey(AuthAccount authAccount) : ResponseHandler<String>
        + deleteKey(String apiKey) : ResponseHandler<String>
    }
     class AuthService {
        - accRepository : AccountRepository
        + createAccount(AuthAccount authAccount) : ResponseHandler<String>
        + regenerateAPIKey(AuthAccount authAccount) : ResponseHandler<String>
        + deleteAccount(String apiKey) : boolean
    }
     class Credentials {
        - Credentials()
        + {static} generateApiKey(AuthAccount account) : String
        + {static} validateApiKey(String apiKey) : void
        + {static} validateCredentials(AuthAccount auth) : void
        + {static} extractLimit(Optional<Integer> limit) : int
        + {static} containsNumber(String value) : boolean
        + {static} isValidUsername(String username) : boolean
    }
     AuthController --> AuthService
}

@enduml

Plattformen#

Es gibt zwei Plattformen, welche abgefragt werden können. Diese sind in zwei unterschiedliche Klassen unterteilt, wobei sie von einer Hauptklasse abgeleitet wurden. Sie wurden in Controller und Service unterteilt, damit der Service von anderen Klassen aufgerufen werden kann. So ist dieser nicht an den REST-Endpunkt gebunden und kann vom GraphQL verwendet werden.

Platformen

@startuml "Platformen"

package Base {
    abstract BaseController {
        # service : final BaseService
        ..
        # BaseController(BaseService service)
        + status()() : ResponseHandler<String>
        + getuser(String username, String apiKey) : ResponseHandler<User>
        + getPosts(String username, String apiKey, int limit) : ResponseHandler<Set<Post>>
    }
    abstract BaseService {
        - platform : final Platform
        - rateLimiter : final RateLimiter
        # webClient : final WebClient
        # accountRepository : AccountRepository
        # userRepository : UserRepository
        # postRepository : PostRepository
        ..
        # BaseService(Platform platform)
        # BaseService(Platform platform, WebClient webClient)
        + {static} getBaseWebClient() : Builder
        + getUser(String apiKey, String username) : ResponseHandler<User>
        + getPosts(String apiKey, String username, int limit) : ResponseHandler<Set<Post>>
        + getJsonData(String requestUri, String apiKey, String username) : ResponseHandler<String>
        - {static} handleResponse(long accountId, ClientResponse response, Platform platform) : Mono<String>
        .. abstract ..
        + {abstract} mapJsonToUser(String json) : User
        + {abstract} mapJsonToPosts(String json, int limit) : Set<Post>
        # {abstract} getUserRequestUri(String username) : String
        # {abstract} getPostsRequestUri(String username, int limit) : String
    }
    BaseController --> BaseService
}

frame Platforms {
    package Instagram {
        class InstagramController extends BaseController{
            + InstagramController(InstagramService service)
            + status() : ResponseHandler<String>
            + getUser(String username, String apiKey) : ResponseHandler<User>
            + getPosts(String username, String apiKey, int limit) : ResponseHandler<Set<Post>>
        }
        class InstagramService extends BaseService {
            + InstagramService()
            - {static} extractLinkedUsers(JsonNode jsonNode, String description) : Set<String>
            - {static} extractCommentsCount(JsonNode jsonNode) : int
            - {static} extractAiPrediction(JsonNode jsonNode) : String
            - {static} extractMedia(JsonNode mediaNode) : Media
            .. Overrides ..
            + mapJsonToUser(String json) : User
            + mapJsonToPosts(String json, int limit) : Set<Post>
            # getUserRequestUri(String username) : String
            # getPostsRequestUri(String username, int limit) : String
        }
        InstagramController --> InstagramService
    }
    package Twitter {
        class TwitterController extends BaseController{
            + TwitterController(TwitterService service)
            + status() : ResponseHandler<String>
            + getUser(String username, String apiKey) : ResponseHandler<User>
            + getPosts(String username, String apiKey, int limit) : ResponseHandler<Set<Post>>
        }
        class TwitterService extends BaseService {
            + TwitterService(String bearer)
            - {static} extractLinkedUsers(JsonNode entities) : Set<String>
            - {static} extractAiPrediction(JsonNode jsonNode) : String
            - {static} extractMedias(JsonNode includeNode) : Map<String, Media>
            - {static} addMediasToPost(Map<String, Media> mediaMap, JsonNode node, Post post, String username) : void
            .. Overrides ..
            + mapJsonToUser(String json) : User
            + mapJsonToPosts(String json, int limit) : Set<Post>
            # getUserRequestUri(String username) : String
            # getPostsRequestUri(String username, int limit) : String
        }
        TwitterController --> TwitterService
    }
    package GraphQL {
        class GraphQLEndpoint {
            - instagramService : InstagramService
            - twitterService : TwitterService
            ..
            + user(String apiKey, String username, Platform platform) : User
            + posts(DgsDataFetchingEnvironment dfe, Optional<Integer> limit) : Set<Post>
            - handleResponse(ResponseHandler<T> responseHandler) : T
            - {static} getParentArgument(DgsDataFetchingEnvironment dfe, String name) : String
        }
        GraphQLEndpoint --> InstagramService
        GraphQLEndpoint --> TwitterService
    }
}

@enduml

Utils#

Für kleinere Methoden oder Klassen, welche öfters verwendet werden wurde die Kategorie Utils entworfen. Diese Klassen beinhalten Methoden, welche von mehreren Klassen gleichzeitig verwendet werden. Unter anderem auch RateLimiter, was die Anzahl der Abfragen pro 15 Minute begrenzt. Die Klasse ResponseHandler wird als Rückgabe-Klasse verwendet, um die Antworten der API zu formatieren. Damit kann überprüft werden ob eine Abfrage gelungen ist oder eine Fehlermeldung zurückgegeben wurde.

Utils

@startuml "Utils"

package Utils {
    class Helper {
        - Helper()
        + {static} trimToSize(String value, int size) : String
        + {static} toUpperCaseNullable(String value) : String
        + {static} equalsIgnoreCase(String value, String other) : boolean
    }

    class RateLimiter << (S,#FF7700) Singleton>> {
        - {static} LIMIT : final int = 900
        - {static} rateLimiterMap : HashBiMap<Platform, RateLimiter>
        - {static} bucketMap : final ConcurrentMap<Long, Bucket>
        ..
        - RateLimiter()
        + {static} synchronized getInstance(Platform platform) : RateLimiter
        - {static} createBucket() : Bucket
        + tryConsume(long accountId) : boolean
        ~ {static} resetRateLimiter() : void
    }

    class ResponseHandler<<T>> {
        - data : final T
        - message : final String
        - status : final HttpStatus
        ..
        + ResponseHandler(String message, HttpStatus status)
        + ResponseHandler(T data, String message, HttpStatus status)
        + generateResponse() : ResponseEntity<Object>
        + {static} generateResponse(String message, HttpStatus status) : ResponseEntity<Object>
        + {static} generateResponse(Object data, String message, HttpStatus status) : ResponseEntity<Object>
    }

    RateLimiter --> RateLimiter : Singleton
}

@enduml

Tests#

Das Projekt ist mit diversen Unit Tests ausgestattet, welche sicherstellen, dass die API korrekt funktioniert. Die Tests wurden mit Mockito und JUnit erstellt und werden automatisch bei einem Commit ausgeführt. Der Status der Tests kann unter diesem Link eingesehen werden.