From 2937c61c345ce14ac34be1b8255c279aef15bfe8 Mon Sep 17 00:00:00 2001 From: Armin Stanitzok <21990230+GODrums@users.noreply.github.com> Date: Mon, 2 Sep 2024 09:28:41 +0200 Subject: [PATCH] Add first GitHub entities and automatically fetch them for the leaderboard (#62) Co-authored-by: Felix T.J. Dietrich Co-authored-by: Felix T.J. Dietrich --- .gitignore | 7 +- server/application-server/HELP.md | 58 ++-- server/application-server/openapi.yaml | 283 ++++++++++++++++++ server/application-server/pom.xml | 14 + .../tum/in/www1/hephaestus/Application.java | 1 - ...assImportIntegratorIntegratorProvider.java | 29 ++ .../in/www1/hephaestus/SecurityConfig.java | 18 +- .../codereview/base/BaseGitServiceEntity.java | 26 ++ .../base/BaseGitServiceEntityConverter.java | 45 +++ .../codereview/comment/IssueComment.java | 42 +++ .../comment/IssueCommentConverter.java | 20 ++ .../codereview/comment/IssueCommentDTO.java | 14 + .../comment/IssueCommentRepository.java | 7 + .../codereview/pullrequest/IssueState.java | 5 + .../codereview/pullrequest/PullRequest.java | 59 ++++ .../pullrequest/PullRequestController.java | 31 ++ .../pullrequest/PullRequestConverter.java | 36 +++ .../pullrequest/PullRequestDTO.java | 18 ++ .../pullrequest/PullRequestRepository.java | 12 + .../pullrequest/PullRequestService.java | 30 ++ .../codereview/repository/Repository.java | 50 ++++ .../repository/RepositoryConverter.java | 39 +++ .../codereview/repository/RepositoryDTO.java | 12 + .../repository/RepositoryRepository.java | 10 + .../repository/RepositoryVisibility.java | 5 + .../www1/hephaestus/codereview/user/User.java | 71 +++++ .../codereview/user/UserController.java | 26 ++ .../codereview/user/UserConverter.java | 33 ++ .../hephaestus/codereview/user/UserDTO.java | 16 + .../codereview/user/UserRepository.java | 20 ++ .../codereview/user/UserService.java | 28 ++ .../www1/hephaestus/hello/HelloService.java | 3 +- .../scheduler/GitHubDataSyncScheduler.java | 55 ++++ .../scheduler/GitHubDataSyncService.java | 172 +++++++++++ .../src/main/resources/application.yml | 54 ++-- .../modules/openapi/.openapi-generator/FILES | 12 + .../src/app/core/modules/openapi/api/api.ts | 8 +- .../openapi/api/pull-request.service.ts | 224 ++++++++++++++ .../api/pull-request.serviceInterface.ts | 41 +++ .../core/modules/openapi/api/user.service.ts | 161 ++++++++++ .../openapi/api/user.serviceInterface.ts | 34 +++ .../openapi/model/issue-comment-dto.ts | 24 ++ .../modules/openapi/model/issue-comment.ts | 24 ++ .../app/core/modules/openapi/model/models.ts | 8 + .../modules/openapi/model/pull-request-dto.ts | 37 +++ .../modules/openapi/model/pull-request.ts | 40 +++ .../modules/openapi/model/repository-dto.ts | 20 ++ .../core/modules/openapi/model/repository.ts | 36 +++ .../core/modules/openapi/model/user-dto.ts | 25 ++ .../app/core/modules/openapi/model/user.ts | 40 +++ 50 files changed, 2027 insertions(+), 56 deletions(-) create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/ClassImportIntegratorIntegratorProvider.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/base/BaseGitServiceEntity.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/base/BaseGitServiceEntityConverter.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueComment.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueCommentConverter.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueCommentDTO.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueCommentRepository.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/IssueState.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequest.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestController.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestConverter.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestDTO.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestRepository.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestService.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/Repository.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryConverter.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryDTO.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryRepository.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryVisibility.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/User.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserController.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserConverter.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserDTO.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserRepository.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserService.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncScheduler.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncService.java create mode 100644 webapp/src/app/core/modules/openapi/api/pull-request.service.ts create mode 100644 webapp/src/app/core/modules/openapi/api/pull-request.serviceInterface.ts create mode 100644 webapp/src/app/core/modules/openapi/api/user.service.ts create mode 100644 webapp/src/app/core/modules/openapi/api/user.serviceInterface.ts create mode 100644 webapp/src/app/core/modules/openapi/model/issue-comment-dto.ts create mode 100644 webapp/src/app/core/modules/openapi/model/issue-comment.ts create mode 100644 webapp/src/app/core/modules/openapi/model/pull-request-dto.ts create mode 100644 webapp/src/app/core/modules/openapi/model/pull-request.ts create mode 100644 webapp/src/app/core/modules/openapi/model/repository-dto.ts create mode 100644 webapp/src/app/core/modules/openapi/model/repository.ts create mode 100644 webapp/src/app/core/modules/openapi/model/user-dto.ts create mode 100644 webapp/src/app/core/modules/openapi/model/user.ts diff --git a/.gitignore b/.gitignore index af353c7f..f62e12af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ .venv .DS_Store +# Idea files +.idea/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -162,4 +165,6 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -node_modules/ \ No newline at end of file +node_modules/ + +application-local.yml \ No newline at end of file diff --git a/server/application-server/HELP.md b/server/application-server/HELP.md index 03b59ce7..66b8f80d 100644 --- a/server/application-server/HELP.md +++ b/server/application-server/HELP.md @@ -1,38 +1,53 @@ # Getting Started +### Local development + +The Spring Boot Maven Plugin supports running your application with a local development environment. To do this, you can use the `spring-boot:run` goal: + +```shell +mvn spring-boot:run +``` + +This will start your application in a local development environment. You can access the application at `http://localhost:8080`. + +Setting environment variables works through profile-based `application.yml` files. For local development, create a `application-local.yml` file overwriting the original properties. See [Using the Plugin :: Spring Boot](https://docs.spring.io/spring-boot/maven-plugin/using.html#using.overriding-command-line) for more information. + ### Reference Documentation + For further reference, please consider the following sections: -* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) -* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/3.3.2/maven-plugin/reference/html/) -* [Create an OCI image](https://docs.spring.io/spring-boot/docs/3.3.2/maven-plugin/reference/html/#build-image) -* [Docker Compose Support](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#features.docker-compose) -* [Spring Boot DevTools](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#using.devtools) -* [Spring Web](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#web) -* [Spring Security](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#web.security) -* [Spring Data JPA](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#data.sql.jpa-and-spring-data) -* [Spring Boot Actuator](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#actuator) -* [Liquibase Migration](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#howto.data-initialization.migration-tool.liquibase) -* [OAuth2 Client](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#web.security.oauth2.client) -* [OAuth2 Resource Server](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#web.security.oauth2.server) +- [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +- [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/3.3.2/maven-plugin/reference/html/) +- [Create an OCI image](https://docs.spring.io/spring-boot/docs/3.3.2/maven-plugin/reference/html/#build-image) +- [Docker Compose Support](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#features.docker-compose) +- [Spring Boot DevTools](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#using.devtools) +- [Spring Web](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#web) +- [Spring Security](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#web.security) +- [Spring Data JPA](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#data.sql.jpa-and-spring-data) +- [Spring Boot Actuator](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#actuator) +- [Liquibase Migration](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#howto.data-initialization.migration-tool.liquibase) +- [OAuth2 Client](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#web.security.oauth2.client) +- [OAuth2 Resource Server](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#web.security.oauth2.server) ### Guides + The following guides illustrate how to use some features concretely: -* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) -* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) -* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) -* [Securing a Web Application](https://spring.io/guides/gs/securing-web/) -* [Spring Boot and OAuth2](https://spring.io/guides/tutorials/spring-boot-oauth2/) -* [Authenticating a User with LDAP](https://spring.io/guides/gs/authenticating-ldap/) -* [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) -* [Building a RESTful Web Service with Spring Boot Actuator](https://spring.io/guides/gs/actuator-service/) +- [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) +- [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) +- [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) +- [Securing a Web Application](https://spring.io/guides/gs/securing-web/) +- [Spring Boot and OAuth2](https://spring.io/guides/tutorials/spring-boot-oauth2/) +- [Authenticating a User with LDAP](https://spring.io/guides/gs/authenticating-ldap/) +- [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) +- [Building a RESTful Web Service with Spring Boot Actuator](https://spring.io/guides/gs/actuator-service/) ### Docker Compose support + This project contains a Docker Compose file named `compose.yaml`. In this file, the following services have been defined: -* postgres: [`postgres:latest`](https://hub.docker.com/_/postgres) +- postgres: [`postgres:latest`](https://hub.docker.com/_/postgres) Please review the tags of the used images and set them to the same as you're running in production. @@ -42,4 +57,3 @@ Due to Maven's design, elements are inherited from the parent POM to the project While most of the inheritance is fine, it also inherits unwanted elements like `` and `` from the parent. To prevent this, the project POM contains empty overrides for these elements. If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides. - diff --git a/server/application-server/openapi.yaml b/server/application-server/openapi.yaml index b9d8cfbe..22c86182 100644 --- a/server/application-server/openapi.yaml +++ b/server/application-server/openapi.yaml @@ -42,6 +42,64 @@ paths: application/json: schema: $ref: "#/components/schemas/Hello" + /user/{login}: + get: + tags: + - user + operationId: getUser + parameters: + - name: login + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/UserDTO" + /pullrequest/{id}: + get: + tags: + - pull-request + operationId: getPullRequest + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/PullRequest" + /pullrequest/author/{login}: + get: + tags: + - pull-request + operationId: getPullRequestsByAuthor + parameters: + - name: login + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + uniqueItems: true + type: array + items: + $ref: "#/components/schemas/PullRequest" /admin: get: tags: @@ -69,3 +127,228 @@ components: Timestamp of when the Hello entity was created. This field is mandatory. format: date-time + IssueCommentDTO: + type: object + properties: + id: + type: integer + format: int64 + body: + type: string + createdAt: + type: string + updatedAt: + type: string + author: + $ref: "#/components/schemas/UserDTO" + pullRequest: + $ref: "#/components/schemas/PullRequestDTO" + PullRequestDTO: + type: object + properties: + id: + type: integer + format: int64 + title: + type: string + url: + type: string + state: + type: string + enum: + - CLOSED + - OPEN + createdAt: + type: string + updatedAt: + type: string + mergedAt: + type: string + author: + $ref: "#/components/schemas/UserDTO" + comments: + uniqueItems: true + type: array + items: + $ref: "#/components/schemas/IssueCommentDTO" + repository: + $ref: "#/components/schemas/RepositoryDTO" + RepositoryDTO: + type: object + properties: + name: + type: string + nameWithOwner: + type: string + description: + type: string + url: + type: string + UserDTO: + type: object + properties: + id: + type: integer + format: int64 + login: + type: string + email: + type: string + name: + type: string + url: + type: string + pullRequests: + uniqueItems: true + type: array + items: + $ref: "#/components/schemas/PullRequestDTO" + comments: + uniqueItems: true + type: array + items: + $ref: "#/components/schemas/IssueCommentDTO" + IssueComment: + required: + - body + type: object + properties: + id: + type: integer + format: int64 + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + body: + type: string + author: + $ref: "#/components/schemas/User" + pullRequest: + $ref: "#/components/schemas/PullRequest" + PullRequest: + required: + - state + - title + - url + type: object + properties: + id: + type: integer + format: int64 + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + title: + type: string + url: + type: string + state: + type: string + description: |- + State of the PullRequest. + Does not include the state of the merge. + enum: + - CLOSED + - OPEN + mergedAt: + type: string + author: + $ref: "#/components/schemas/User" + comments: + uniqueItems: true + type: array + items: + $ref: "#/components/schemas/IssueComment" + repository: + $ref: "#/components/schemas/Repository" + Repository: + required: + - defaultBranch + - description + - name + - nameWithOwner + - url + - visibility + type: object + properties: + id: + type: integer + format: int64 + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + name: + type: string + nameWithOwner: + type: string + description: + type: string + defaultBranch: + type: string + visibility: + type: string + enum: + - PUBLIC + - PRIVATE + url: + type: string + homepage: + type: string + pullRequests: + uniqueItems: true + type: array + items: + $ref: "#/components/schemas/PullRequest" + User: + required: + - login + - url + type: object + properties: + id: + type: integer + format: int64 + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + login: + type: string + description: Unique login identifier for a user. + email: + type: string + name: + type: string + description: Display name of the user. + url: + type: string + description: |- + Unique URL to the user's profile. + Not the website a user can set in their profile. + avatarUrl: + type: string + description: |- + URL to the user's avatar. + If unavailable, a fallback can be generated from the login, e.g. on Github: + https://github.com/{login}.png + pullRequests: + uniqueItems: true + type: array + items: + $ref: "#/components/schemas/PullRequest" + comments: + uniqueItems: true + type: array + items: + $ref: "#/components/schemas/IssueComment" diff --git a/server/application-server/pom.xml b/server/application-server/pom.xml index 727462a9..d13c8cac 100644 --- a/server/application-server/pom.xml +++ b/server/application-server/pom.xml @@ -29,6 +29,7 @@ 21 + local,dev @@ -120,6 +121,16 @@ spring-modulith-starter-test test + + org.kohsuke + github-api + 1.324 + + + io.hypersistence + hypersistence-utils-hibernate-63 + 3.8.2 + @@ -158,6 +169,7 @@ org.springframework.boot spring-boot-maven-plugin + ${app.profiles} -Dspring.application.admin.enabled=true @@ -165,6 +177,8 @@ lombok + 500 + 100 diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/Application.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/Application.java index 0d39f321..b56dce19 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/Application.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/Application.java @@ -11,5 +11,4 @@ public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } - } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/ClassImportIntegratorIntegratorProvider.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/ClassImportIntegratorIntegratorProvider.java new file mode 100644 index 00000000..ec7ca8bb --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/ClassImportIntegratorIntegratorProvider.java @@ -0,0 +1,29 @@ +package de.tum.in.www1.hephaestus; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.jpa.boot.spi.IntegratorProvider; + +import de.tum.in.www1.hephaestus.codereview.comment.IssueCommentDTO; +import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequestDTO; +import de.tum.in.www1.hephaestus.codereview.repository.RepositoryDTO; +import de.tum.in.www1.hephaestus.codereview.user.UserDTO; +import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; + +public class ClassImportIntegratorIntegratorProvider implements IntegratorProvider { + + @Override + public List getIntegrators() { + // Accessible DTOs + @SuppressWarnings("rawtypes") + List classes = new ArrayList<>(); + classes.add(UserDTO.class); + classes.add(PullRequestDTO.class); + classes.add(IssueCommentDTO.class); + classes.add(RepositoryDTO.class); + + return List.of(new ClassImportIntegrator(classes)); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/SecurityConfig.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/SecurityConfig.java index 04a1e0f3..7bb18cae 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/SecurityConfig.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/SecurityConfig.java @@ -1,6 +1,5 @@ package de.tum.in.www1.hephaestus; -import java.util.Arrays; import java.util.List; import org.springframework.context.annotation.Bean; @@ -20,14 +19,13 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .csrf(csrf -> csrf.disable()) - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - .authorizeHttpRequests(authorizeRequests -> authorizeRequests - .requestMatchers("/admin/**").authenticated() - .anyRequest().permitAll() - ) - .formLogin(Customizer.withDefaults()) - .logout(Customizer.withDefaults()); + .csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .authorizeHttpRequests(authorizeRequests -> authorizeRequests + .requestMatchers("/admin/**").authenticated() + .anyRequest().permitAll()) + .formLogin(Customizer.withDefaults()) + .logout(Customizer.withDefaults()); return http.build(); } @@ -38,7 +36,7 @@ CorsConfigurationSource corsConfigurationSource() { configuration.applyPermitDefaultValues(); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**",configuration); + source.registerCorsConfiguration("/**", configuration); return source; } } \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/base/BaseGitServiceEntity.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/base/BaseGitServiceEntity.java new file mode 100644 index 00000000..d66dc62f --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/base/BaseGitServiceEntity.java @@ -0,0 +1,26 @@ +package de.tum.in.www1.hephaestus.codereview.base; + +import java.time.OffsetDateTime; + +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@MappedSuperclass +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public abstract class BaseGitServiceEntity { + @Id + protected Long id; + + protected OffsetDateTime createdAt; + + protected OffsetDateTime updatedAt; +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/base/BaseGitServiceEntityConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/base/BaseGitServiceEntityConverter.java new file mode 100644 index 00000000..82aa9b98 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/base/BaseGitServiceEntityConverter.java @@ -0,0 +1,45 @@ +package de.tum.in.www1.hephaestus.codereview.base; + +import org.kohsuke.github.GHObject; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Date; + +@ReadingConverter +public abstract class BaseGitServiceEntityConverter + implements Converter { + + private static final Logger logger = LoggerFactory.getLogger(BaseGitServiceEntityConverter.class); + + protected void convertBaseFields(S source, T target) { + if (source == null || target == null) { + throw new IllegalArgumentException("Source and target must not be null"); + } + + // Map common fields + target.setId(source.getId()); + + try { + target.setCreatedAt(convertToOffsetDateTime(source.getCreatedAt())); + } catch (IOException e) { + logger.error("Failed to convert createdAt field for source {}: {}", source.getId(), e.getMessage()); + target.setCreatedAt(null); + } + + try { + target.setUpdatedAt(convertToOffsetDateTime(source.getUpdatedAt())); + } catch (IOException e) { + logger.error("Failed to convert updatedAt field for source {}: {}", source.getId(), e.getMessage()); + target.setUpdatedAt(null); + } + } + + private OffsetDateTime convertToOffsetDateTime(Date date) { + return date != null ? date.toInstant().atOffset(ZoneOffset.UTC) : null; + } +} \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueComment.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueComment.java new file mode 100644 index 00000000..fa89fa43 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueComment.java @@ -0,0 +1,42 @@ +package de.tum.in.www1.hephaestus.codereview.comment; + +import jakarta.persistence.Table; + +import org.springframework.lang.NonNull; + +import de.tum.in.www1.hephaestus.codereview.base.BaseGitServiceEntity; +import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequest; +import de.tum.in.www1.hephaestus.codereview.user.User; +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Entity +@Table(name = "issue_comment") +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class IssueComment extends BaseGitServiceEntity { + @NonNull + @Lob + @Basic(fetch = FetchType.EAGER) + private String body; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author_id") + @ToString.Exclude + private User author; + + @ManyToOne(optional = false) + @JoinColumn(name = "pullrequest_id", referencedColumnName = "id") + @ToString.Exclude + private PullRequest pullRequest; +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueCommentConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueCommentConverter.java new file mode 100644 index 00000000..1cef439d --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueCommentConverter.java @@ -0,0 +1,20 @@ +package de.tum.in.www1.hephaestus.codereview.comment; + +import org.kohsuke.github.GHIssueComment; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +import de.tum.in.www1.hephaestus.codereview.base.BaseGitServiceEntityConverter; + +@Component +public class IssueCommentConverter extends BaseGitServiceEntityConverter { + + @Override + public IssueComment convert(@NonNull GHIssueComment source) { + IssueComment comment = new IssueComment(); + convertBaseFields(source, comment); + comment.setBody(source.getBody()); + return comment; + } + +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueCommentDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueCommentDTO.java new file mode 100644 index 00000000..6e2b537f --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueCommentDTO.java @@ -0,0 +1,14 @@ +package de.tum.in.www1.hephaestus.codereview.comment; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequestDTO; +import de.tum.in.www1.hephaestus.codereview.user.UserDTO; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record IssueCommentDTO(Long id, String body, String createdAt, String updatedAt, UserDTO author, + PullRequestDTO pullRequest) { + public IssueCommentDTO(Long id, String body, String createdAt, String updatedAt) { + this(id, body, createdAt, updatedAt, null, null); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueCommentRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueCommentRepository.java new file mode 100644 index 00000000..adacc0d7 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/comment/IssueCommentRepository.java @@ -0,0 +1,7 @@ +package de.tum.in.www1.hephaestus.codereview.comment; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface IssueCommentRepository extends JpaRepository { + +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/IssueState.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/IssueState.java new file mode 100644 index 00000000..07ae6da8 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/IssueState.java @@ -0,0 +1,5 @@ +package de.tum.in.www1.hephaestus.codereview.pullrequest; + +public enum IssueState { + CLOSED, OPEN +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequest.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequest.java new file mode 100644 index 00000000..564c1ce4 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequest.java @@ -0,0 +1,59 @@ +package de.tum.in.www1.hephaestus.codereview.pullrequest; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.lang.NonNull; + +import de.tum.in.www1.hephaestus.codereview.base.BaseGitServiceEntity; +import de.tum.in.www1.hephaestus.codereview.comment.IssueComment; +import de.tum.in.www1.hephaestus.codereview.repository.Repository; +import de.tum.in.www1.hephaestus.codereview.user.User; +import jakarta.persistence.Table; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Entity +@Table(name = "pull_request") +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class PullRequest extends BaseGitServiceEntity { + @NonNull + private String title; + + @NonNull + private String url; + + /** + * State of the PullRequest. + * Does not include the state of the merge. + */ + @NonNull + private IssueState state; + + private String mergedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author_id") + @ToString.Exclude + private User author; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "pullRequest") + @ToString.Exclude + private Set comments = new HashSet<>();; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "repository_id", referencedColumnName = "id") + @ToString.Exclude + private Repository repository; +} \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestController.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestController.java new file mode 100644 index 00000000..8af92a91 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestController.java @@ -0,0 +1,31 @@ +package de.tum.in.www1.hephaestus.codereview.pullrequest; + +import java.util.Optional; +import java.util.Set; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/pullrequest") +public class PullRequestController { + private final PullRequestService pullRequestService; + + public PullRequestController(PullRequestService pullRequestService) { + this.pullRequestService = pullRequestService; + } + + @GetMapping("/{id}") + public ResponseEntity getPullRequest(@PathVariable Long id) { + Optional pullRequest =pullRequestService.getPullRequestById(id); + return pullRequest.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); + } + + @GetMapping("/author/{login}") + public Set getPullRequestsByAuthor(@PathVariable String login) { + return pullRequestService.getPullRequestsByAuthor(login); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestConverter.java new file mode 100644 index 00000000..d779a1ac --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestConverter.java @@ -0,0 +1,36 @@ +package de.tum.in.www1.hephaestus.codereview.pullrequest; + +import org.kohsuke.github.GHPullRequest; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +import de.tum.in.www1.hephaestus.codereview.base.BaseGitServiceEntityConverter; + +@Component +public class PullRequestConverter extends BaseGitServiceEntityConverter { + @Override + public PullRequest convert(@NonNull GHPullRequest source) { + IssueState state = convertState(source.getState()); + PullRequest pullRequest = new PullRequest(); + convertBaseFields(source, pullRequest); + pullRequest.setTitle(source.getTitle()); + pullRequest.setUrl(source.getHtmlUrl().toString()); + pullRequest.setState(state); + if (source.getMergedAt() != null) { + pullRequest.setMergedAt(source.getMergedAt().toString()); + } + return pullRequest; + } + + private IssueState convertState(org.kohsuke.github.GHIssueState state) { + switch (state) { + case OPEN: + return IssueState.OPEN; + case CLOSED: + return IssueState.CLOSED; + default: + return IssueState.OPEN; + } + } + +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestDTO.java new file mode 100644 index 00000000..9e85f02c --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestDTO.java @@ -0,0 +1,18 @@ +package de.tum.in.www1.hephaestus.codereview.pullrequest; + +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.hephaestus.codereview.comment.IssueCommentDTO; +import de.tum.in.www1.hephaestus.codereview.repository.RepositoryDTO; +import de.tum.in.www1.hephaestus.codereview.user.UserDTO; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record PullRequestDTO(Long id, String title, String url, IssueState state, String createdAt, String updatedAt, + String mergedAt, UserDTO author, Set comments, RepositoryDTO repository) { + public PullRequestDTO(Long id, String title, String url, IssueState state, String createdAt, String updatedAt, + String mergedAt) { + this(id, title, url, state, createdAt, updatedAt, mergedAt, null, null, null); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestRepository.java new file mode 100644 index 00000000..255d1749 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestRepository.java @@ -0,0 +1,12 @@ +package de.tum.in.www1.hephaestus.codereview.pullrequest; + +import java.util.Set; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PullRequestRepository extends JpaRepository { + + Set findByAuthor_Login(String authorLogin); +} \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestService.java new file mode 100644 index 00000000..4c3fdbf3 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/pullrequest/PullRequestService.java @@ -0,0 +1,30 @@ +package de.tum.in.www1.hephaestus.codereview.pullrequest; + +import java.util.Optional; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class PullRequestService { + private static final Logger logger = LoggerFactory.getLogger(PullRequestService.class); + + private final PullRequestRepository pullRequestRepository; + + public PullRequestService(PullRequestRepository pullRequestRepository) { + this.pullRequestRepository = pullRequestRepository; + } + + public Optional getPullRequestById(Long id) { + logger.info("Getting pullRequest with id: {}", id); + return pullRequestRepository.findById(id); + } + + public Set getPullRequestsByAuthor(String login) { + logger.info("Getting pullRequest by author: {}", login); + return pullRequestRepository.findByAuthor_Login(login); + } + +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/Repository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/Repository.java new file mode 100644 index 00000000..2ebad800 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/Repository.java @@ -0,0 +1,50 @@ +package de.tum.in.www1.hephaestus.codereview.repository; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.lang.NonNull; + +import de.tum.in.www1.hephaestus.codereview.base.BaseGitServiceEntity; +import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequest; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Entity +@Table(name = "repository") +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class Repository extends BaseGitServiceEntity { + @NonNull + private String name; + + @NonNull + private String nameWithOwner; + + @NonNull + private String description; + + @NonNull + String defaultBranch; + + @NonNull + private RepositoryVisibility visibility; + + @NonNull + private String url; + + String homepage; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "repository", fetch = FetchType.EAGER) + @ToString.Exclude + private Set pullRequests = new HashSet<>(); +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryConverter.java new file mode 100644 index 00000000..2482b505 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryConverter.java @@ -0,0 +1,39 @@ +package de.tum.in.www1.hephaestus.codereview.repository; + +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHRepository.Visibility; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; + +import de.tum.in.www1.hephaestus.codereview.base.BaseGitServiceEntityConverter; + +@Component +public class RepositoryConverter extends BaseGitServiceEntityConverter { + + @Override + @Nullable + public Repository convert(@NonNull GHRepository source) { + Repository repository = new Repository(); + convertBaseFields(source, repository); + repository.setName(source.getName()); + repository.setNameWithOwner(source.getFullName()); + repository.setDescription(source.getDescription()); + repository.setUrl(source.getHtmlUrl().toString()); + repository.setDefaultBranch(source.getDefaultBranch()); + repository.setVisibility(convertVisibility(source.getVisibility())); + repository.setHomepage(source.getHomepage()); + return repository; + } + + private RepositoryVisibility convertVisibility(Visibility visibility) { + switch (visibility) { + case PRIVATE: + return RepositoryVisibility.PRIVATE; + case PUBLIC: + return RepositoryVisibility.PUBLIC; + default: + return RepositoryVisibility.PRIVATE; + } + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryDTO.java new file mode 100644 index 00000000..5defadbe --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryDTO.java @@ -0,0 +1,12 @@ +package de.tum.in.www1.hephaestus.codereview.repository; + +import java.util.Set; + +import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequestDTO; + +public record RepositoryDTO(String name, String nameWithOwner, String description, String url, + Set pullRequests) { + public RepositoryDTO(String name, String nameWithOwner, String description, String url) { + this(name, nameWithOwner, description, url, null); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryRepository.java new file mode 100644 index 00000000..836d8ffb --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryRepository.java @@ -0,0 +1,10 @@ +package de.tum.in.www1.hephaestus.codereview.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +@org.springframework.stereotype.Repository +public interface RepositoryRepository + extends JpaRepository { + + Repository findByNameWithOwner(String nameWithOwner); +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryVisibility.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryVisibility.java new file mode 100644 index 00000000..83143a1b --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/repository/RepositoryVisibility.java @@ -0,0 +1,5 @@ +package de.tum.in.www1.hephaestus.codereview.repository; + +public enum RepositoryVisibility { + PUBLIC, PRIVATE +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/User.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/User.java new file mode 100644 index 00000000..e0180ade --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/User.java @@ -0,0 +1,71 @@ +package de.tum.in.www1.hephaestus.codereview.user; + +import jakarta.persistence.Table; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.lang.NonNull; + +import de.tum.in.www1.hephaestus.codereview.base.BaseGitServiceEntity; +import de.tum.in.www1.hephaestus.codereview.comment.IssueComment; +import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequest; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.OneToMany; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Entity +@Table(name = "user", schema = "public") +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class User extends BaseGitServiceEntity { + /** + * Unique login identifier for a user. + */ + @NonNull + private String login; + + @Column + private String email; + + /** + * Display name of the user. + */ + @Column + private String name; + + /** + * Unique URL to the user's profile. + * Not the website a user can set in their profile. + */ + @NonNull + private String url; + + /** + * URL to the user's avatar. + * If unavailable, a fallback can be generated from the login, e.g. on Github: + * https://github.com/{login}.png + */ + private String avatarUrl; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "author") + private Set pullRequests = new HashSet<>(); + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "author") + private Set comments = new HashSet<>(); + + public void addComment(IssueComment comment) { + comments.add(comment); + } + + public void addPullRequest(PullRequest pullRequest) { + pullRequests.add(pullRequest); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserController.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserController.java new file mode 100644 index 00000000..bc31c831 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserController.java @@ -0,0 +1,26 @@ +package de.tum.in.www1.hephaestus.codereview.user; + +import java.util.Optional; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/user") +public class UserController { + private final UserService userService; + + public UserController(UserService actorService) { + this.userService = actorService; + } + + @GetMapping("/{login}") + public ResponseEntity getUser(@PathVariable String login) { + Optional user = userService.getUserDTO(login); + return user.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); + } + +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserConverter.java new file mode 100644 index 00000000..2d26e0fb --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserConverter.java @@ -0,0 +1,33 @@ +package de.tum.in.www1.hephaestus.codereview.user; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +import de.tum.in.www1.hephaestus.codereview.base.BaseGitServiceEntityConverter; + +@Component +public class UserConverter extends BaseGitServiceEntityConverter { + + protected static final Logger logger = LoggerFactory.getLogger(UserConverter.class); + + @Override + public User convert(@NonNull org.kohsuke.github.GHUser source) { + User user = new User(); + convertBaseFields(source, user); + user.setLogin(source.getLogin()); + user.setUrl(source.getHtmlUrl().toString()); + user.setAvatarUrl(source.getAvatarUrl()); + try { + user.setName(source.getName()); + user.setEmail(source.getEmail()); + } catch (IOException e) { + logger.error("Failed to convert user fields for source {}: {}", source.getId(), e.getMessage()); + } + return user; + } + +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserDTO.java new file mode 100644 index 00000000..64d8051b --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserDTO.java @@ -0,0 +1,16 @@ +package de.tum.in.www1.hephaestus.codereview.user; + +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.hephaestus.codereview.comment.IssueCommentDTO; +import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequestDTO; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record UserDTO(Long id, String login, String email, String name, String url, Set pullRequests, + Set comments) { + public UserDTO(Long id, String login, String email, String name, String url) { + this(id, login, email, name, url, null, null); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserRepository.java new file mode 100644 index 00000000..64696cb4 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserRepository.java @@ -0,0 +1,20 @@ +package de.tum.in.www1.hephaestus.codereview.user; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface UserRepository extends JpaRepository { + + @Query("SELECT u FROM User u WHERE u.login = :login") + Optional findUser(@Param("login") String login); + + @Query(""" + SELECT new UserDTO(u.id, u.login, u.email, u.name, u.url) + FROM User u + WHERE u.login = :login + """) + Optional findByLogin(@Param("login") String login); +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserService.java new file mode 100644 index 00000000..1c3703a3 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/codereview/user/UserService.java @@ -0,0 +1,28 @@ +package de.tum.in.www1.hephaestus.codereview.user; + +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class UserService { + private static final Logger logger = LoggerFactory.getLogger(UserService.class); + + private final UserRepository userRepository; + + public UserService(UserRepository actorRepository) { + this.userRepository = actorRepository; + } + + public Optional getUser(String login) { + logger.info("Getting user with login: " + login); + return userRepository.findUser(login); + } + + public Optional getUserDTO(String login) { + logger.info("Getting userDTO with login: " + login); + return userRepository.findByLogin(login); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/hello/HelloService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/hello/HelloService.java index b8d394b4..e3f6d072 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/hello/HelloService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/hello/HelloService.java @@ -30,7 +30,8 @@ public List getAllHellos() { } /** - * Creates a new {@link Hello} entity with the current timestamp and saves it to the repository. + * Creates a new {@link Hello} entity with the current timestamp and saves it to + * the repository. * * @return The created Hello entity */ diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncScheduler.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncScheduler.java new file mode 100644 index 00000000..acd1c2d9 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncScheduler.java @@ -0,0 +1,55 @@ +package de.tum.in.www1.hephaestus.scheduler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; + +import java.io.IOException; + +@Component +public class GitHubDataSyncScheduler { + + private static final Logger logger = LoggerFactory.getLogger(GitHubDataSyncScheduler.class); + private final GitHubDataSyncService dataSyncService; + + @Value("${monitoring.runOnStartup:true}") + private boolean runOnStartup; + + @Value("${monitoring.repositories}") + private String[] repositoriesToMonitor; + + public GitHubDataSyncScheduler(GitHubDataSyncService dataSyncService) { + this.dataSyncService = dataSyncService; + } + + @PostConstruct + public void onStartup() { + if (runOnStartup) { + logger.info("Starting initial GitHub data sync for Hephaestus..."); + syncAllRepositories(); + logger.info("Initial GitHub data sync completed successfully."); + } + } + + @Scheduled(cron = "${monitoring.repository-sync-cron}") + public void syncDataCron() { + logger.info("Starting daily GitHub data sync..."); + syncAllRepositories(); + logger.info("Daily GitHub data sync completed successfully."); + } + + private void syncAllRepositories() { + for (String repositoryName : repositoriesToMonitor) { + try { + dataSyncService.syncData(repositoryName); + logger.info("GitHub data sync completed successfully for repository: " + repositoryName); + } catch (IOException e) { + logger.error("Error during GitHub data sync: ", e); + } + } + } +} \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncService.java new file mode 100644 index 00000000..a0ff6bf8 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/scheduler/GitHubDataSyncService.java @@ -0,0 +1,172 @@ +package de.tum.in.www1.hephaestus.scheduler; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.hephaestus.codereview.comment.IssueComment; +import de.tum.in.www1.hephaestus.codereview.comment.IssueCommentConverter; +import de.tum.in.www1.hephaestus.codereview.comment.IssueCommentRepository; +import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequest; +import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequestConverter; +import de.tum.in.www1.hephaestus.codereview.pullrequest.PullRequestRepository; +import de.tum.in.www1.hephaestus.codereview.repository.Repository; +import de.tum.in.www1.hephaestus.codereview.repository.RepositoryConverter; +import de.tum.in.www1.hephaestus.codereview.repository.RepositoryRepository; +import de.tum.in.www1.hephaestus.codereview.user.User; +import de.tum.in.www1.hephaestus.codereview.user.UserConverter; +import de.tum.in.www1.hephaestus.codereview.user.UserRepository; + +@Service +public class GitHubDataSyncService { + private static final Logger logger = LoggerFactory.getLogger(GitHubDataSyncService.class); + + @Value("${github.authToken:null}") + private String ghAuthToken; + + private GitHub github; + + private final RepositoryRepository repositoryRepository; + private final PullRequestRepository pullRequestRepository; + private final IssueCommentRepository commentRepository; + private final UserRepository userRepository; + + private final RepositoryConverter repositoryConverter; + private final PullRequestConverter pullRequestConverter; + private final IssueCommentConverter commentConverter; + private final UserConverter userConverter; + + public GitHubDataSyncService(RepositoryRepository repositoryRepository, PullRequestRepository pullRequestRepository, + IssueCommentRepository commentRepository, UserRepository userRepository, + RepositoryConverter repositoryConverter, PullRequestConverter pullRequestConverter, + IssueCommentConverter commentConverter, UserConverter userConverter) { + logger.info("Hello from GitHubDataSyncService!"); + + this.repositoryRepository = repositoryRepository; + this.pullRequestRepository = pullRequestRepository; + this.commentRepository = commentRepository; + this.userRepository = userRepository; + + this.repositoryConverter = repositoryConverter; + this.pullRequestConverter = pullRequestConverter; + this.commentConverter = commentConverter; + this.userConverter = userConverter; + } + + public void syncData(String repositoryName) throws IOException { + if (ghAuthToken == null || ghAuthToken.isEmpty() || ghAuthToken.equals("null")) { + logger.error("No GitHub auth token provided!"); + return; + } + if (github == null) { + github = new GitHubBuilder().withOAuthToken(ghAuthToken).build(); + } + Repository repo = this.fetchRepository(repositoryName); + logger.info("Synced repository until " + repo.getUpdatedAt()); + } + + /** + * Rest API implementation of fetching a Github repository. + * + * @return The repository corresponding to the given nameWithOwner. + * @throws IOException + */ + public Repository fetchRepository(String nameWithOwner) throws IOException { + if (github == null) { + logger.error("GitHub client not initialized correctly!"); + return null; + } + + // Avoid double fetching of the same repository + Repository existingRepository = repositoryRepository.findByNameWithOwner(nameWithOwner); + if (existingRepository != null) { + logger.info("Found existing repository: " + existingRepository); + return existingRepository; + } + + GHRepository ghRepo = github.getRepository(nameWithOwner); + Repository repository = repositoryConverter.convert(ghRepo); + if (repository == null) { + logger.error("Error while fetching repository!"); + return null; + } + // preliminary save to make it referenceable + repositoryRepository.save(repository); + + // Retrieve PRs in pages of 10 + Set prs = ghRepo.queryPullRequests().list().withPageSize(10).toList().stream().map(pr -> { + PullRequest pullRequest = pullRequestConverter.convert(pr); + pullRequest.setRepository(repository); + pullRequestRepository.save(pullRequest); + try { + Set comments = getCommentsFromGHPullRequest(pr, pullRequest); + pullRequest.setComments(comments); + commentRepository.saveAll(comments); + } catch (IOException e) { + logger.error("Error while fetching PR comments!"); + pullRequest.setComments(new HashSet<>()); + } + try { + pullRequest.setAuthor(getActorFromGHUser(pr.getUser())); + } catch (IOException e) { + logger.error("Error while fetching PR author!"); + pullRequest.setAuthor(null); + } + + return pullRequest; + }).collect(Collectors.toSet()); + repository.setPullRequests(prs); + pullRequestRepository.saveAll(prs); + repositoryRepository.save(repository); + return repository; + } + + /** + * Retrieves the comments of a given pull request. + * + * @param pr The GH pull request. + * @param pullRequest Stored PR to which the comments belong. + * @return The comments of the given pull request. + * @throws IOException + */ + private Set getCommentsFromGHPullRequest(GHPullRequest pr, PullRequest pullRequest) + throws IOException { + Set comments = pr.queryComments().list().toList().stream() + .map(comment -> { + IssueComment c = commentConverter.convert(comment); + c.setPullRequest(pullRequest); + User author; + try { + author = getActorFromGHUser(comment.getUser()); + author.addComment(c); + author.addPullRequest(pullRequest); + } catch (IOException e) { + logger.error("Error while fetching author!"); + author = null; + } + c.setAuthor(author); + return c; + }).collect(Collectors.toSet()); + return comments; + } + + private User getActorFromGHUser(org.kohsuke.github.GHUser user) { + User ghUser = userRepository.findUser(user.getLogin()).orElse(null); + if (ghUser == null) { + ghUser = userConverter.convert(user); + userRepository.save(ghUser); + } + return ghUser; + + } +} diff --git a/server/application-server/src/main/resources/application.yml b/server/application-server/src/main/resources/application.yml index ab369fa7..6feb75f9 100644 --- a/server/application-server/src/main/resources/application.yml +++ b/server/application-server/src/main/resources/application.yml @@ -1,26 +1,40 @@ spring: - application: - name: Hephaestus + application: + name: Hephaestus - datasource: - url: jdbc:postgresql://localhost:5432/hephaestus - username: root - password: root - - jpa: - hibernate: - ddl-auto: create-drop - properties: - hibernate: - dialect: org.hibernate.dialect.PostgreSQLDialect + datasource: + url: jdbc:postgresql://localhost:5432/hephaestus + username: root + password: root - liquibase: - change-log: classpath:db/master.xml + jpa: + hibernate: + # ddl-auto: update + ddl-auto: create-drop + # ddl-auto: none + # show-sql: "true" + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + integrator_provider: de.tum.in.www1.hephaestus.ClassImportIntegratorIntegratorProvider + format_sql: "true" - security: - user: - name: admin - password: admin + liquibase: + change-log: classpath:db/master.xml + + security: + user: + name: admin + password: admin springdoc: - default-produces-media-type: application/json + default-produces-media-type: application/json + +monitoring: + runOnStartup: false + repository-sync-cron: "0 0 3 * * ?" + repositories: ls1intum/Artemis, ls1intum/Pyris, ls1intum/Athena, ls1intum/Athena-CoFee, ls1intum/artemis-ansible-collection, ls1intum/Ares, ls1intum/Ares2, ls1intum/Aeolus, ls1intum/hades, ls1intum/Apollon, ls1intum/Hephaestus, ls1intum/Apollon_standalone + +# Can be any OAuth token, such as the PAT +github: + authToken: null diff --git a/webapp/src/app/core/modules/openapi/.openapi-generator/FILES b/webapp/src/app/core/modules/openapi/.openapi-generator/FILES index a74bc16f..3b9ff517 100644 --- a/webapp/src/app/core/modules/openapi/.openapi-generator/FILES +++ b/webapp/src/app/core/modules/openapi/.openapi-generator/FILES @@ -7,11 +7,23 @@ api/admin.serviceInterface.ts api/api.ts api/hello.service.ts api/hello.serviceInterface.ts +api/pull-request.service.ts +api/pull-request.serviceInterface.ts +api/user.service.ts +api/user.serviceInterface.ts configuration.ts encoder.ts git_push.sh index.ts model/hello.ts +model/issue-comment-dto.ts +model/issue-comment.ts model/models.ts +model/pull-request-dto.ts +model/pull-request.ts +model/repository-dto.ts +model/repository.ts +model/user-dto.ts +model/user.ts param.ts variables.ts diff --git a/webapp/src/app/core/modules/openapi/api/api.ts b/webapp/src/app/core/modules/openapi/api/api.ts index 496ee4fc..49c6e262 100644 --- a/webapp/src/app/core/modules/openapi/api/api.ts +++ b/webapp/src/app/core/modules/openapi/api/api.ts @@ -4,4 +4,10 @@ export * from './admin.serviceInterface'; export * from './hello.service'; import { HelloService } from './hello.service'; export * from './hello.serviceInterface'; -export const APIS = [AdminService, HelloService]; +export * from './pull-request.service'; +import { PullRequestService } from './pull-request.service'; +export * from './pull-request.serviceInterface'; +export * from './user.service'; +import { UserService } from './user.service'; +export * from './user.serviceInterface'; +export const APIS = [AdminService, HelloService, PullRequestService, UserService]; diff --git a/webapp/src/app/core/modules/openapi/api/pull-request.service.ts b/webapp/src/app/core/modules/openapi/api/pull-request.service.ts new file mode 100644 index 00000000..5511e994 --- /dev/null +++ b/webapp/src/app/core/modules/openapi/api/pull-request.service.ts @@ -0,0 +1,224 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { PullRequest } from '../model/pull-request'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { + PullRequestServiceInterface +} from './pull-request.serviceInterface'; + + + +@Injectable({ + providedIn: 'root' +}) +export class PullRequestService implements PullRequestServiceInterface { + + protected basePath = 'http://localhost'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + const firstBasePath = Array.isArray(basePath) ? basePath[0] : undefined; + if (firstBasePath != undefined) { + basePath = firstBasePath; + } + + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + // @ts-ignore + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, (value as Date).toISOString().substring(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * @param id + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getPullRequest(id: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getPullRequest(id: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getPullRequest(id: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getPullRequest(id: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (id === null || id === undefined) { + throw new Error('Required parameter id was null or undefined when calling getPullRequest.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/pullrequest/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * @param login + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getPullRequestsByAuthor(login: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getPullRequestsByAuthor(login: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getPullRequestsByAuthor(login: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getPullRequestsByAuthor(login: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (login === null || login === undefined) { + throw new Error('Required parameter login was null or undefined when calling getPullRequestsByAuthor.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/pullrequest/author/${this.configuration.encodeParam({name: "login", value: login, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}`; + return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/webapp/src/app/core/modules/openapi/api/pull-request.serviceInterface.ts b/webapp/src/app/core/modules/openapi/api/pull-request.serviceInterface.ts new file mode 100644 index 00000000..66d5615b --- /dev/null +++ b/webapp/src/app/core/modules/openapi/api/pull-request.serviceInterface.ts @@ -0,0 +1,41 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { HttpHeaders } from '@angular/common/http'; + +import { Observable } from 'rxjs'; + +import { PullRequest } from '../model/models'; + + +import { Configuration } from '../configuration'; + + + +export interface PullRequestServiceInterface { + defaultHeaders: HttpHeaders; + configuration: Configuration; + + /** + * + * + * @param id + */ + getPullRequest(id: number, extraHttpRequestParams?: any): Observable; + + /** + * + * + * @param login + */ + getPullRequestsByAuthor(login: string, extraHttpRequestParams?: any): Observable>; + +} diff --git a/webapp/src/app/core/modules/openapi/api/user.service.ts b/webapp/src/app/core/modules/openapi/api/user.service.ts new file mode 100644 index 00000000..4806a28d --- /dev/null +++ b/webapp/src/app/core/modules/openapi/api/user.service.ts @@ -0,0 +1,161 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { UserDTO } from '../model/user-dto'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { + UserServiceInterface +} from './user.serviceInterface'; + + + +@Injectable({ + providedIn: 'root' +}) +export class UserService implements UserServiceInterface { + + protected basePath = 'http://localhost'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + const firstBasePath = Array.isArray(basePath) ? basePath[0] : undefined; + if (firstBasePath != undefined) { + basePath = firstBasePath; + } + + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + // @ts-ignore + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, (value as Date).toISOString().substring(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * @param login + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getUser(login: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getUser(login: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getUser(login: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getUser(login: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (login === null || login === undefined) { + throw new Error('Required parameter login was null or undefined when calling getUser.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/user/${this.configuration.encodeParam({name: "login", value: login, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/webapp/src/app/core/modules/openapi/api/user.serviceInterface.ts b/webapp/src/app/core/modules/openapi/api/user.serviceInterface.ts new file mode 100644 index 00000000..2221661e --- /dev/null +++ b/webapp/src/app/core/modules/openapi/api/user.serviceInterface.ts @@ -0,0 +1,34 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { HttpHeaders } from '@angular/common/http'; + +import { Observable } from 'rxjs'; + +import { UserDTO } from '../model/models'; + + +import { Configuration } from '../configuration'; + + + +export interface UserServiceInterface { + defaultHeaders: HttpHeaders; + configuration: Configuration; + + /** + * + * + * @param login + */ + getUser(login: string, extraHttpRequestParams?: any): Observable; + +} diff --git a/webapp/src/app/core/modules/openapi/model/issue-comment-dto.ts b/webapp/src/app/core/modules/openapi/model/issue-comment-dto.ts new file mode 100644 index 00000000..b1f01fb9 --- /dev/null +++ b/webapp/src/app/core/modules/openapi/model/issue-comment-dto.ts @@ -0,0 +1,24 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { PullRequestDTO } from './pull-request-dto'; +import { UserDTO } from './user-dto'; + + +export interface IssueCommentDTO { + id?: number; + body?: string; + createdAt?: string; + updatedAt?: string; + author?: UserDTO; + pullRequest?: PullRequestDTO; +} + diff --git a/webapp/src/app/core/modules/openapi/model/issue-comment.ts b/webapp/src/app/core/modules/openapi/model/issue-comment.ts new file mode 100644 index 00000000..5c4eaadd --- /dev/null +++ b/webapp/src/app/core/modules/openapi/model/issue-comment.ts @@ -0,0 +1,24 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { User } from './user'; +import { PullRequest } from './pull-request'; + + +export interface IssueComment { + id?: number; + createdAt?: string; + updatedAt?: string; + body: string; + author?: User; + pullRequest?: PullRequest; +} + diff --git a/webapp/src/app/core/modules/openapi/model/models.ts b/webapp/src/app/core/modules/openapi/model/models.ts index 3d51a59b..cea96097 100644 --- a/webapp/src/app/core/modules/openapi/model/models.ts +++ b/webapp/src/app/core/modules/openapi/model/models.ts @@ -1 +1,9 @@ export * from './hello'; +export * from './issue-comment'; +export * from './issue-comment-dto'; +export * from './pull-request'; +export * from './pull-request-dto'; +export * from './repository'; +export * from './repository-dto'; +export * from './user'; +export * from './user-dto'; diff --git a/webapp/src/app/core/modules/openapi/model/pull-request-dto.ts b/webapp/src/app/core/modules/openapi/model/pull-request-dto.ts new file mode 100644 index 00000000..4a1064f1 --- /dev/null +++ b/webapp/src/app/core/modules/openapi/model/pull-request-dto.ts @@ -0,0 +1,37 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { UserDTO } from './user-dto'; +import { IssueCommentDTO } from './issue-comment-dto'; +import { RepositoryDTO } from './repository-dto'; + + +export interface PullRequestDTO { + id?: number; + title?: string; + url?: string; + state?: PullRequestDTO.StateEnum; + createdAt?: string; + updatedAt?: string; + mergedAt?: string; + author?: UserDTO; + comments?: Set; + repository?: RepositoryDTO; +} +export namespace PullRequestDTO { + export type StateEnum = 'CLOSED' | 'OPEN'; + export const StateEnum = { + Closed: 'CLOSED' as StateEnum, + Open: 'OPEN' as StateEnum + }; +} + + diff --git a/webapp/src/app/core/modules/openapi/model/pull-request.ts b/webapp/src/app/core/modules/openapi/model/pull-request.ts new file mode 100644 index 00000000..e57a8082 --- /dev/null +++ b/webapp/src/app/core/modules/openapi/model/pull-request.ts @@ -0,0 +1,40 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { Repository } from './repository'; +import { User } from './user'; +import { IssueComment } from './issue-comment'; + + +export interface PullRequest { + id?: number; + createdAt?: string; + updatedAt?: string; + title: string; + url: string; + /** + * State of the PullRequest. Does not include the state of the merge. + */ + state: PullRequest.StateEnum; + mergedAt?: string; + author?: User; + comments?: Set; + repository?: Repository; +} +export namespace PullRequest { + export type StateEnum = 'CLOSED' | 'OPEN'; + export const StateEnum = { + Closed: 'CLOSED' as StateEnum, + Open: 'OPEN' as StateEnum + }; +} + + diff --git a/webapp/src/app/core/modules/openapi/model/repository-dto.ts b/webapp/src/app/core/modules/openapi/model/repository-dto.ts new file mode 100644 index 00000000..bb31547f --- /dev/null +++ b/webapp/src/app/core/modules/openapi/model/repository-dto.ts @@ -0,0 +1,20 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface RepositoryDTO { + name?: string; + nameWithOwner?: string; + description?: string; + url?: string; +} + diff --git a/webapp/src/app/core/modules/openapi/model/repository.ts b/webapp/src/app/core/modules/openapi/model/repository.ts new file mode 100644 index 00000000..a202a249 --- /dev/null +++ b/webapp/src/app/core/modules/openapi/model/repository.ts @@ -0,0 +1,36 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { PullRequest } from './pull-request'; + + +export interface Repository { + id?: number; + createdAt?: string; + updatedAt?: string; + name: string; + nameWithOwner: string; + description: string; + defaultBranch: string; + visibility: Repository.VisibilityEnum; + url: string; + homepage?: string; + pullRequests?: Set; +} +export namespace Repository { + export type VisibilityEnum = 'PUBLIC' | 'PRIVATE'; + export const VisibilityEnum = { + Public: 'PUBLIC' as VisibilityEnum, + Private: 'PRIVATE' as VisibilityEnum + }; +} + + diff --git a/webapp/src/app/core/modules/openapi/model/user-dto.ts b/webapp/src/app/core/modules/openapi/model/user-dto.ts new file mode 100644 index 00000000..aaaae01b --- /dev/null +++ b/webapp/src/app/core/modules/openapi/model/user-dto.ts @@ -0,0 +1,25 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { PullRequestDTO } from './pull-request-dto'; +import { IssueCommentDTO } from './issue-comment-dto'; + + +export interface UserDTO { + id?: number; + login?: string; + email?: string; + name?: string; + url?: string; + pullRequests?: Set; + comments?: Set; +} + diff --git a/webapp/src/app/core/modules/openapi/model/user.ts b/webapp/src/app/core/modules/openapi/model/user.ts new file mode 100644 index 00000000..d156b36f --- /dev/null +++ b/webapp/src/app/core/modules/openapi/model/user.ts @@ -0,0 +1,40 @@ +/** + * Hephaestus API + * API documentation for the Hephaestus application server. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { PullRequest } from './pull-request'; +import { IssueComment } from './issue-comment'; + + +export interface User { + id?: number; + createdAt?: string; + updatedAt?: string; + /** + * Unique login identifier for a user. + */ + login: string; + email?: string; + /** + * Display name of the user. + */ + name?: string; + /** + * Unique URL to the user\'s profile. Not the website a user can set in their profile. + */ + url: string; + /** + * URL to the user\'s avatar. If unavailable, a fallback can be generated from the login, e.g. on Github: https://github.com/{login}.png + */ + avatarUrl?: string; + pullRequests?: Set; + comments?: Set; +} +