diff --git a/README.md b/README.md index c747c5757..5ca7b125e 100644 --- a/README.md +++ b/README.md @@ -8,74 +8,285 @@ This codebase was created to demonstrate a fully fledged full-stack application For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo. -# *NEW* GraphQL Support +## Features -Following some DDD principles. REST or GraphQL is just a kind of adapter. And the domain layer will be consistent all the time. So this repository implement GraphQL and REST at the same time. +This application provides a complete backend implementation for a Medium-like blogging platform with the following features: -The GraphQL schema is https://github.com/gothinkster/spring-boot-realworld-example-app/blob/master/src/main/resources/schema/schema.graphqls and the visualization looks like below. +- User registration and authentication with JWT tokens +- User profiles with follow/unfollow functionality +- Article CRUD operations with slug-based URLs +- Article tagging system +- Article favorites +- Comments on articles +- Feed of articles from followed users +- Both REST API and GraphQL endpoints -![](graphql-schema.png) - -And this implementation is using [dgs-framework](https://github.com/Netflix/dgs-framework) which is a quite new java graphql server framework. -# How it works +## GraphQL Support -The application uses Spring Boot (Web, Mybatis). +Following some DDD principles, REST or GraphQL is just a kind of adapter. The domain layer remains consistent regardless of the API type. This repository implements both GraphQL and REST simultaneously. -* Use the idea of Domain Driven Design to separate the business term and infrastructure term. -* Use MyBatis to implement the [Data Mapper](https://martinfowler.com/eaaCatalog/dataMapper.html) pattern for persistence. -* Use [CQRS](https://martinfowler.com/bliki/CQRS.html) pattern to separate the read model and write model. +The GraphQL schema is located at `src/main/resources/schema/schema.graphqls` and the visualization looks like below. -And the code is organized as this: +![](graphql-schema.png) -1. `api` is the web layer implemented by Spring MVC -2. `core` is the business model including entities and services -3. `application` is the high-level services for querying the data transfer objects -4. `infrastructure` contains all the implementation classes as the technique details +This implementation uses [dgs-framework](https://github.com/Netflix/dgs-framework), Netflix's GraphQL server framework for Spring Boot. -# Security +### How it works -Integration with Spring Security and add other filter for jwt token process. +The application uses Spring Boot (Web, Mybatis). -The secret key is stored in `application.properties`. +- Uses Domain Driven Design (DDD) to separate business logic from infrastructure concerns +- Uses MyBatis to implement the [Data Mapper](https://martinfowler.com/eaaCatalog/dataMapper.html) pattern for persistence +- Uses [CQRS](https://martinfowler.com/bliki/CQRS.html) pattern to separate read and write models +- Uses Lombok to reduce boilerplate code + +## Project Structure + +``` +src/main/java/io/spring/ +├── api/ # REST API layer (Spring MVC controllers) +│ ├── exception/ # Custom exceptions and error handling +│ └── security/ # JWT filter and security configuration +├── application/ # Application services layer +│ ├── article/ # Article command services and validation +│ ├── data/ # Data Transfer Objects (DTOs) +│ └── user/ # User command services and validation +├── core/ # Domain layer (entities and repository interfaces) +│ ├── article/ # Article entity and repository +│ ├── comment/ # Comment entity and repository +│ ├── favorite/ # ArticleFavorite entity and repository +│ ├── service/ # Domain service interfaces (JwtService, AuthorizationService) +│ └── user/ # User entity, FollowRelation, and repository +├── graphql/ # GraphQL layer (Netflix DGS datafetchers and mutations) +│ └── exception/ # GraphQL-specific exceptions +└── infrastructure/ # Infrastructure layer (implementations) + ├── mybatis/ # MyBatis mappers, type handlers, and read services + ├── repository/ # Repository implementations + └── service/ # Service implementations (DefaultJwtService) +``` + +## Requirements + +- Java 11 or higher +- Gradle (wrapper included) + +## Dependencies + +The main dependencies used in this project: + +| Dependency | Purpose | +|------------|---------| +| Spring Boot 2.6.3 | Application framework | +| Spring Security | Authentication and authorization | +| MyBatis | ORM/Data Mapper | +| Netflix DGS | GraphQL server framework | +| JJWT | JWT token generation and validation | +| Joda-Time | Date/time handling | +| SQLite | Database (default) | +| Lombok | Boilerplate code reduction | +| Flyway | Database migrations | + +## Security + +Integration with Spring Security with JWT token-based authentication. + +The JWT configuration is stored in `application.properties`: +- `jwt.secret`: Secret key for signing tokens (HS512 algorithm) +- `jwt.sessionTime`: Token expiration time in seconds (default: 86400 = 24 hours) + +## Database + +Uses SQLite database by default for easy local testing without losing data after restart. Can be changed in `application.properties` for any other database. + +## Getting started -# Database - -It uses a ~~H2 in-memory database~~ sqlite database (for easy local test without losing test data after every restart), can be changed easily in the `application.properties` for any other database. +You'll need Java 11 installed. -# Getting started +```bash +./gradlew bootRun +``` -You'll need Java 11 installed. +To test that it works, open a browser tab at http://localhost:8080/tags or run: - ./gradlew bootRun +```bash +curl http://localhost:8080/tags +``` -To test that it works, open a browser tab at http://localhost:8080/tags . -Alternatively, you can run +## Configuration - curl http://localhost:8080/tags +The main configuration file is `src/main/resources/application.properties`: -# Try it out with [Docker](https://www.docker.com/) +```properties +# Database +spring.datasource.url=jdbc:sqlite:dev.db +spring.datasource.driver-class-name=org.sqlite.JDBC + +# JWT +jwt.secret=your-secret-key +jwt.sessionTime=86400 + +# MyBatis +mybatis.configuration.map-underscore-to-camel-case=true +mybatis.mapper-locations=mapper/*.xml +``` + +## API Examples + +### User Registration + +```bash +curl -X POST http://localhost:8080/users \ + -H "Content-Type: application/json" \ + -d '{ + "user": { + "username": "johndoe", + "email": "john@example.com", + "password": "password123" + } + }' +``` + +### User Login + +```bash +curl -X POST http://localhost:8080/users/login \ + -H "Content-Type: application/json" \ + -d '{ + "user": { + "email": "john@example.com", + "password": "password123" + } + }' +``` + +### Get Current User + +```bash +curl http://localhost:8080/user \ + -H "Authorization: Token " +``` + +### Create Article + +```bash +curl -X POST http://localhost:8080/articles \ + -H "Content-Type: application/json" \ + -H "Authorization: Token " \ + -d '{ + "article": { + "title": "How to train your dragon", + "description": "Ever wonder how?", + "body": "You have to believe", + "tagList": ["dragons", "training"] + } + }' +``` + +### Get Articles + +```bash +# Get all articles +curl http://localhost:8080/articles + +# Filter by tag +curl http://localhost:8080/articles?tag=dragons + +# Filter by author +curl http://localhost:8080/articles?author=johndoe + +# Filter by favorited +curl http://localhost:8080/articles?favorited=johndoe + +# Pagination +curl http://localhost:8080/articles?limit=10&offset=0 +``` + +### Get User Feed + +```bash +curl http://localhost:8080/articles/feed \ + -H "Authorization: Token " +``` + +### Favorite Article + +```bash +curl -X POST http://localhost:8080/articles/how-to-train-your-dragon/favorite \ + -H "Authorization: Token " +``` + +### Follow User + +```bash +curl -X POST http://localhost:8080/profiles/johndoe/follow \ + -H "Authorization: Token " +``` + +### Add Comment + +```bash +curl -X POST http://localhost:8080/articles/how-to-train-your-dragon/comments \ + -H "Content-Type: application/json" \ + -H "Authorization: Token " \ + -d '{ + "comment": { + "body": "Great article!" + } + }' +``` + +## GraphQL API + +The GraphQL endpoint is available at `http://localhost:8080/graphql`. + +Example query: + +```graphql +query { + articles(first: 10) { + edges { + node { + slug + title + description + author { + username + } + } + } + } +} +``` + +## Try it out with Docker You'll need Docker installed. - - ./gradlew bootBuildImage --imageName spring-boot-realworld-example-app - docker run -p 8081:8080 spring-boot-realworld-example-app -# Try it out with a RealWorld frontend +```bash +./gradlew bootBuildImage --imageName spring-boot-realworld-example-app +docker run -p 8081:8080 spring-boot-realworld-example-app +``` + +## Try it out with a RealWorld frontend The entry point address of the backend API is at http://localhost:8080, **not** http://localhost:8080/api as some of the frontend documentation suggests. -# Run test +## Run tests -The repository contains a lot of test cases to cover both api test and repository test. +The repository contains comprehensive test cases covering both API tests and repository tests. - ./gradlew test +```bash +./gradlew test +``` -# Code format +## Code format -Use spotless for code format. +Use Spotless for code formatting with Google Java Format. - ./gradlew spotlessJavaApply +```bash +./gradlew spotlessJavaApply +``` -# Help +## Contributing Please fork and PR to improve the project. diff --git a/src/main/java/io/spring/JacksonCustomizations.java b/src/main/java/io/spring/JacksonCustomizations.java index 86fab0abe..6ade43f05 100644 --- a/src/main/java/io/spring/JacksonCustomizations.java +++ b/src/main/java/io/spring/JacksonCustomizations.java @@ -11,26 +11,72 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +/** + * Configuration class for customizing Jackson JSON serialization. + * + *

This class provides custom Jackson modules and serializers to handle + * specific data types used in the RealWorld application, particularly + * Joda-Time DateTime objects. + * + * @see RealWorldModules + * @see DateTimeSerializer + */ @Configuration public class JacksonCustomizations { + /** + * Creates and registers the custom Jackson module for the RealWorld application. + * + * @return a Jackson module containing custom serializers + */ @Bean public Module realWorldModules() { return new RealWorldModules(); } + /** + * Custom Jackson module that registers serializers for RealWorld-specific types. + * + *

This module registers a custom serializer for Joda-Time DateTime objects + * to ensure consistent date/time formatting across the API. + */ public static class RealWorldModules extends SimpleModule { + /** + * Creates a new RealWorldModules instance and registers custom serializers. + */ public RealWorldModules() { addSerializer(DateTime.class, new DateTimeSerializer()); } } + /** + * Custom Jackson serializer for Joda-Time DateTime objects. + * + *

This serializer converts DateTime objects to ISO 8601 formatted strings + * in UTC timezone, ensuring consistent date/time representation in JSON responses. + * + *

Example output: "2023-01-15T10:30:00.000Z" + */ public static class DateTimeSerializer extends StdSerializer { + /** + * Creates a new DateTimeSerializer instance. + */ protected DateTimeSerializer() { super(DateTime.class); } + /** + * Serializes a DateTime object to JSON. + * + *

The DateTime is formatted as an ISO 8601 string in UTC timezone. + * Null values are serialized as JSON null. + * + * @param value the DateTime value to serialize + * @param gen the JSON generator + * @param provider the serializer provider + * @throws IOException if an I/O error occurs during serialization + */ @Override public void serialize(DateTime value, JsonGenerator gen, SerializerProvider provider) throws IOException { diff --git a/src/main/java/io/spring/MyBatisConfig.java b/src/main/java/io/spring/MyBatisConfig.java index d1f741cfb..d3a04fbd5 100644 --- a/src/main/java/io/spring/MyBatisConfig.java +++ b/src/main/java/io/spring/MyBatisConfig.java @@ -3,6 +3,19 @@ import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; +/** + * Configuration class for MyBatis integration with Spring. + * + *

This class enables transaction management for the application, allowing + * MyBatis operations to participate in Spring-managed transactions. The + * {@code @EnableTransactionManagement} annotation enables Spring's annotation-driven + * transaction management capability. + * + *

MyBatis mapper interfaces and XML mappings are automatically discovered + * through Spring Boot's auto-configuration mechanism. + * + * @see org.springframework.transaction.annotation.Transactional + */ @Configuration @EnableTransactionManagement public class MyBatisConfig {} diff --git a/src/main/java/io/spring/RealWorldApplication.java b/src/main/java/io/spring/RealWorldApplication.java index c9f34e1a5..36abd7360 100644 --- a/src/main/java/io/spring/RealWorldApplication.java +++ b/src/main/java/io/spring/RealWorldApplication.java @@ -3,9 +3,38 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * Main entry point for the RealWorld Spring Boot application. + * + *

This is a demonstration application that implements the RealWorld API specification, + * showcasing a fully-featured full-stack application built with Spring Boot and MyBatis. + * The application includes CRUD operations, authentication, routing, pagination, and more. + * + *

The application follows Domain-Driven Design (DDD) principles and supports both + * REST and GraphQL APIs for accessing the same domain logic. + * + *

Key features: + *

    + *
  • User registration and authentication with JWT
  • + *
  • Article CRUD operations with tagging support
  • + *
  • Comment system for articles
  • + *
  • User following/followers functionality
  • + *
  • Article favoriting system
  • + *
+ * + * @see RealWorld Specification + */ @SpringBootApplication public class RealWorldApplication { + /** + * Application entry point. + * + *

Bootstraps the Spring Boot application by initializing the Spring context + * and starting the embedded web server. + * + * @param args command-line arguments passed to the application + */ public static void main(String[] args) { SpringApplication.run(RealWorldApplication.class, args); } diff --git a/src/main/java/io/spring/Util.java b/src/main/java/io/spring/Util.java index d2512acca..977e0a2ee 100644 --- a/src/main/java/io/spring/Util.java +++ b/src/main/java/io/spring/Util.java @@ -1,6 +1,18 @@ package io.spring; +/** + * Utility class providing common helper methods for the RealWorld application. + * + *

This class contains static utility methods that are used throughout the + * application for common operations like string validation. + */ public class Util { + /** + * Checks if a string value is null or empty. + * + * @param value the string to check + * @return {@code true} if the value is null or empty, {@code false} otherwise + */ public static boolean isEmpty(String value) { return value == null || value.isEmpty(); } diff --git a/src/main/java/io/spring/api/ArticleApi.java b/src/main/java/io/spring/api/ArticleApi.java index c80afa31d..fb311426b 100644 --- a/src/main/java/io/spring/api/ArticleApi.java +++ b/src/main/java/io/spring/api/ArticleApi.java @@ -24,14 +24,43 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * REST API controller for single article operations. + * + *

This controller handles operations on individual articles identified by their slug, + * including retrieving, updating, and deleting articles. + * + *

Endpoints: + *

    + *
  • GET /articles/{slug} - Get an article by slug
  • + *
  • PUT /articles/{slug} - Update an article
  • + *
  • DELETE /articles/{slug} - Delete an article
  • + *
+ * + * @see ArticlesApi + * @see Article + */ @RestController @RequestMapping(path = "/articles/{slug}") @AllArgsConstructor public class ArticleApi { + /** Service for querying article data. */ private ArticleQueryService articleQueryService; + + /** Repository for article persistence operations. */ private ArticleRepository articleRepository; + + /** Service for article write operations. */ private ArticleCommandService articleCommandService; + /** + * Retrieves an article by its slug. + * + * @param slug the URL-friendly slug of the article + * @param user the authenticated user (optional) + * @return the article data wrapped in a response + * @throws ResourceNotFoundException if the article is not found + */ @GetMapping public ResponseEntity article( @PathVariable("slug") String slug, @AuthenticationPrincipal User user) { @@ -41,6 +70,18 @@ public ResponseEntity article( .orElseThrow(ResourceNotFoundException::new); } + /** + * Updates an existing article. + * + *

Only the article's author can update it. + * + * @param slug the URL-friendly slug of the article + * @param user the authenticated user + * @param updateArticleParam the update parameters + * @return the updated article data + * @throws ResourceNotFoundException if the article is not found + * @throws NoAuthorizationException if the user is not the author + */ @PutMapping public ResponseEntity updateArticle( @PathVariable("slug") String slug, @@ -62,6 +103,17 @@ public ResponseEntity updateArticle( .orElseThrow(ResourceNotFoundException::new); } + /** + * Deletes an article. + * + *

Only the article's author can delete it. + * + * @param slug the URL-friendly slug of the article + * @param user the authenticated user + * @return 204 No Content on success + * @throws ResourceNotFoundException if the article is not found + * @throws NoAuthorizationException if the user is not the author + */ @DeleteMapping public ResponseEntity deleteArticle( @PathVariable("slug") String slug, @AuthenticationPrincipal User user) { @@ -78,6 +130,12 @@ public ResponseEntity deleteArticle( .orElseThrow(ResourceNotFoundException::new); } + /** + * Wraps article data in a response map. + * + * @param articleData the article data to wrap + * @return a map containing the article data + */ private Map articleResponse(ArticleData articleData) { return new HashMap() { { diff --git a/src/main/java/io/spring/api/ArticleFavoriteApi.java b/src/main/java/io/spring/api/ArticleFavoriteApi.java index 9c6a37ba4..47785b839 100644 --- a/src/main/java/io/spring/api/ArticleFavoriteApi.java +++ b/src/main/java/io/spring/api/ArticleFavoriteApi.java @@ -18,14 +18,41 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * REST API controller for article favorite operations. + * + *

This controller handles favoriting and unfavoriting articles. + * + *

Endpoints: + *

    + *
  • POST /articles/{slug}/favorite - Favorite an article
  • + *
  • DELETE /articles/{slug}/favorite - Unfavorite an article
  • + *
+ * + * @see ArticleFavorite + * @see Article + */ @RestController @RequestMapping(path = "articles/{slug}/favorite") @AllArgsConstructor public class ArticleFavoriteApi { + /** Repository for article favorite persistence. */ private ArticleFavoriteRepository articleFavoriteRepository; + + /** Repository for article persistence. */ private ArticleRepository articleRepository; + + /** Service for querying article data. */ private ArticleQueryService articleQueryService; + /** + * Favorites an article for the authenticated user. + * + * @param slug the URL-friendly slug of the article + * @param user the authenticated user + * @return the article data with updated favorite status + * @throws ResourceNotFoundException if the article is not found + */ @PostMapping public ResponseEntity favoriteArticle( @PathVariable("slug") String slug, @AuthenticationPrincipal User user) { @@ -36,6 +63,14 @@ public ResponseEntity favoriteArticle( return responseArticleData(articleQueryService.findBySlug(slug, user).get()); } + /** + * Unfavorites an article for the authenticated user. + * + * @param slug the URL-friendly slug of the article + * @param user the authenticated user + * @return the article data with updated favorite status + * @throws ResourceNotFoundException if the article is not found + */ @DeleteMapping public ResponseEntity unfavoriteArticle( @PathVariable("slug") String slug, @AuthenticationPrincipal User user) { @@ -50,6 +85,12 @@ public ResponseEntity unfavoriteArticle( return responseArticleData(articleQueryService.findBySlug(slug, user).get()); } + /** + * Wraps article data in a response entity. + * + * @param articleData the article data to wrap + * @return a response entity containing the article data + */ private ResponseEntity> responseArticleData( final ArticleData articleData) { return ResponseEntity.ok( diff --git a/src/main/java/io/spring/api/ArticlesApi.java b/src/main/java/io/spring/api/ArticlesApi.java index 50584bd6d..829b2049c 100644 --- a/src/main/java/io/spring/api/ArticlesApi.java +++ b/src/main/java/io/spring/api/ArticlesApi.java @@ -18,13 +18,40 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +/** + * REST API controller for article collection operations. + * + *

This controller handles operations on the articles collection, + * including creating new articles, listing articles with filters, and + * retrieving the user's feed. + * + *

Endpoints: + *

    + *
  • POST /articles - Create a new article
  • + *
  • GET /articles - List articles with optional filters
  • + *
  • GET /articles/feed - Get articles from followed users
  • + *
+ * + * @see ArticleApi + * @see Article + */ @RestController @RequestMapping(path = "/articles") @AllArgsConstructor public class ArticlesApi { + /** Service for article write operations. */ private ArticleCommandService articleCommandService; + + /** Service for querying article data. */ private ArticleQueryService articleQueryService; + /** + * Creates a new article. + * + * @param newArticleParam the parameters for the new article + * @param user the authenticated user (author) + * @return the created article data + */ @PostMapping public ResponseEntity createArticle( @Valid @RequestBody NewArticleParam newArticleParam, @AuthenticationPrincipal User user) { @@ -37,6 +64,14 @@ public ResponseEntity createArticle( }); } + /** + * Retrieves the user's feed of articles from followed authors. + * + * @param offset the number of articles to skip (default 0) + * @param limit the maximum number of articles to return (default 20) + * @param user the authenticated user + * @return a paginated list of articles from followed users + */ @GetMapping(path = "feed") public ResponseEntity getFeed( @RequestParam(value = "offset", defaultValue = "0") int offset, @@ -45,6 +80,17 @@ public ResponseEntity getFeed( return ResponseEntity.ok(articleQueryService.findUserFeed(user, new Page(offset, limit))); } + /** + * Retrieves a list of articles with optional filters. + * + * @param offset the number of articles to skip (default 0) + * @param limit the maximum number of articles to return (default 20) + * @param tag filter by tag name (optional) + * @param favoritedBy filter by username who favorited (optional) + * @param author filter by author username (optional) + * @param user the authenticated user (optional) + * @return a paginated list of articles matching the filters + */ @GetMapping public ResponseEntity getArticles( @RequestParam(value = "offset", defaultValue = "0") int offset, diff --git a/src/main/java/io/spring/api/CommentsApi.java b/src/main/java/io/spring/api/CommentsApi.java index c5f7e77e9..8d6c014f8 100644 --- a/src/main/java/io/spring/api/CommentsApi.java +++ b/src/main/java/io/spring/api/CommentsApi.java @@ -29,14 +29,43 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; +/** + * REST API controller for comment operations on articles. + * + *

This controller handles creating, listing, and deleting comments on articles. + * + *

Endpoints: + *

    + *
  • POST /articles/{slug}/comments - Create a comment
  • + *
  • GET /articles/{slug}/comments - List comments on an article
  • + *
  • DELETE /articles/{slug}/comments/{id} - Delete a comment
  • + *
+ * + * @see Comment + * @see Article + */ @RestController @RequestMapping(path = "/articles/{slug}/comments") @AllArgsConstructor public class CommentsApi { + /** Repository for article persistence. */ private ArticleRepository articleRepository; + + /** Repository for comment persistence. */ private CommentRepository commentRepository; + + /** Service for querying comment data. */ private CommentQueryService commentQueryService; + /** + * Creates a new comment on an article. + * + * @param slug the URL-friendly slug of the article + * @param user the authenticated user + * @param newCommentParam the comment content + * @return the created comment data with 201 status + * @throws ResourceNotFoundException if the article is not found + */ @PostMapping public ResponseEntity createComment( @PathVariable("slug") String slug, @@ -50,6 +79,14 @@ public ResponseEntity createComment( .body(commentResponse(commentQueryService.findById(comment.getId(), user).get())); } + /** + * Retrieves all comments on an article. + * + * @param slug the URL-friendly slug of the article + * @param user the authenticated user (optional) + * @return a list of comments on the article + * @throws ResourceNotFoundException if the article is not found + */ @GetMapping public ResponseEntity getComments( @PathVariable("slug") String slug, @AuthenticationPrincipal User user) { @@ -64,6 +101,18 @@ public ResponseEntity getComments( }); } + /** + * Deletes a comment from an article. + * + *

Only the comment author or article author can delete the comment. + * + * @param slug the URL-friendly slug of the article + * @param commentId the ID of the comment to delete + * @param user the authenticated user + * @return 204 No Content on success + * @throws ResourceNotFoundException if the article or comment is not found + * @throws NoAuthorizationException if the user is not authorized + */ @RequestMapping(path = "{id}", method = RequestMethod.DELETE) public ResponseEntity deleteComment( @PathVariable("slug") String slug, @@ -84,6 +133,12 @@ public ResponseEntity deleteComment( .orElseThrow(ResourceNotFoundException::new); } + /** + * Wraps comment data in a response map. + * + * @param commentData the comment data to wrap + * @return a map containing the comment data + */ private Map commentResponse(CommentData commentData) { return new HashMap() { { @@ -93,10 +148,16 @@ private Map commentResponse(CommentData commentData) { } } +/** + * Parameter object for creating a new comment. + * + * @see CommentsApi#createComment(String, User, NewCommentParam) + */ @Getter @NoArgsConstructor @JsonRootName("comment") class NewCommentParam { + /** The content of the comment. Must not be blank. */ @NotBlank(message = "can't be empty") private String body; } diff --git a/src/main/java/io/spring/api/CurrentUserApi.java b/src/main/java/io/spring/api/CurrentUserApi.java index e096aec0b..ffb9df1e2 100644 --- a/src/main/java/io/spring/api/CurrentUserApi.java +++ b/src/main/java/io/spring/api/CurrentUserApi.java @@ -20,14 +20,39 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * REST API controller for current user operations. + * + *

This controller handles operations for the currently authenticated user, + * including retrieving and updating their profile. + * + *

Endpoints: + *

    + *
  • GET /user - Get current user's profile
  • + *
  • PUT /user - Update current user's profile
  • + *
+ * + * @see User + * @see UsersApi + */ @RestController @RequestMapping(path = "/user") @AllArgsConstructor public class CurrentUserApi { + /** Service for querying user data. */ private UserQueryService userQueryService; + + /** Service for user write operations. */ private UserService userService; + /** + * Retrieves the current user's profile. + * + * @param currentUser the authenticated user + * @param authorization the Authorization header containing the JWT token + * @return the user data with the current token + */ @GetMapping public ResponseEntity currentUser( @AuthenticationPrincipal User currentUser, @@ -37,6 +62,14 @@ public ResponseEntity currentUser( userResponse(new UserWithToken(userData, authorization.split(" ")[1]))); } + /** + * Updates the current user's profile. + * + * @param currentUser the authenticated user + * @param token the Authorization header containing the JWT token + * @param updateUserParam the update parameters + * @return the updated user data with the current token + */ @PutMapping public ResponseEntity updateProfile( @AuthenticationPrincipal User currentUser, @@ -48,6 +81,12 @@ public ResponseEntity updateProfile( return ResponseEntity.ok(userResponse(new UserWithToken(userData, token.split(" ")[1]))); } + /** + * Wraps user data with token in a response map. + * + * @param userWithToken the user data with token + * @return a map containing the user data + */ private Map userResponse(UserWithToken userWithToken) { return new HashMap() { { diff --git a/src/main/java/io/spring/api/ProfileApi.java b/src/main/java/io/spring/api/ProfileApi.java index 1cf7bca3b..3f2a017bf 100644 --- a/src/main/java/io/spring/api/ProfileApi.java +++ b/src/main/java/io/spring/api/ProfileApi.java @@ -18,13 +18,39 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * REST API controller for user profile operations. + * + *

This controller handles viewing profiles and managing follow relationships. + * + *

Endpoints: + *

    + *
  • GET /profiles/{username} - Get a user's profile
  • + *
  • POST /profiles/{username}/follow - Follow a user
  • + *
  • DELETE /profiles/{username}/follow - Unfollow a user
  • + *
+ * + * @see ProfileData + * @see FollowRelation + */ @RestController @RequestMapping(path = "profiles/{username}") @AllArgsConstructor public class ProfileApi { + /** Service for querying profile data. */ private ProfileQueryService profileQueryService; + + /** Repository for user persistence and follow relationships. */ private UserRepository userRepository; + /** + * Retrieves a user's profile by username. + * + * @param username the username of the profile to retrieve + * @param user the authenticated user (optional) + * @return the profile data + * @throws ResourceNotFoundException if the user is not found + */ @GetMapping public ResponseEntity getProfile( @PathVariable("username") String username, @AuthenticationPrincipal User user) { @@ -34,6 +60,14 @@ public ResponseEntity getProfile( .orElseThrow(ResourceNotFoundException::new); } + /** + * Follows a user. + * + * @param username the username of the user to follow + * @param user the authenticated user + * @return the profile data with updated following status + * @throws ResourceNotFoundException if the target user is not found + */ @PostMapping(path = "follow") public ResponseEntity follow( @PathVariable("username") String username, @AuthenticationPrincipal User user) { @@ -48,6 +82,14 @@ public ResponseEntity follow( .orElseThrow(ResourceNotFoundException::new); } + /** + * Unfollows a user. + * + * @param username the username of the user to unfollow + * @param user the authenticated user + * @return the profile data with updated following status + * @throws ResourceNotFoundException if the target user or follow relation is not found + */ @DeleteMapping(path = "follow") public ResponseEntity unfollow( @PathVariable("username") String username, @AuthenticationPrincipal User user) { @@ -67,6 +109,12 @@ public ResponseEntity unfollow( } } + /** + * Wraps profile data in a response entity. + * + * @param profile the profile data to wrap + * @return a response entity containing the profile data + */ private ResponseEntity profileResponse(ProfileData profile) { return ResponseEntity.ok( new HashMap() { diff --git a/src/main/java/io/spring/api/TagsApi.java b/src/main/java/io/spring/api/TagsApi.java index e3991832a..d98beb1e4 100644 --- a/src/main/java/io/spring/api/TagsApi.java +++ b/src/main/java/io/spring/api/TagsApi.java @@ -8,12 +8,30 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * REST API controller for tag operations. + * + *

This controller handles retrieving all available tags. + * + *

Endpoints: + *

    + *
  • GET /tags - Get all tags
  • + *
+ * + * @see io.spring.core.article.Tag + */ @RestController @RequestMapping(path = "tags") @AllArgsConstructor public class TagsApi { + /** Service for querying tag data. */ private TagsQueryService tagsQueryService; + /** + * Retrieves all available tags. + * + * @return a list of all tag names + */ @GetMapping public ResponseEntity getTags() { return ResponseEntity.ok( diff --git a/src/main/java/io/spring/api/UsersApi.java b/src/main/java/io/spring/api/UsersApi.java index d91321e82..c88718a29 100644 --- a/src/main/java/io/spring/api/UsersApi.java +++ b/src/main/java/io/spring/api/UsersApi.java @@ -27,15 +27,44 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * REST API controller for user registration and authentication. + * + *

This controller handles user registration and login operations. + * + *

Endpoints: + *

    + *
  • POST /users - Register a new user
  • + *
  • POST /users/login - Authenticate a user
  • + *
+ * + * @see User + * @see CurrentUserApi + */ @RestController @AllArgsConstructor public class UsersApi { + /** Repository for user persistence. */ private UserRepository userRepository; + + /** Service for querying user data. */ private UserQueryService userQueryService; + + /** Encoder for password verification. */ private PasswordEncoder passwordEncoder; + + /** Service for JWT token generation. */ private JwtService jwtService; + + /** Service for user write operations. */ private UserService userService; + /** + * Registers a new user. + * + * @param registerParam the registration parameters + * @return the created user data with JWT token (201 status) + */ @RequestMapping(path = "/users", method = POST) public ResponseEntity createUser(@Valid @RequestBody RegisterParam registerParam) { User user = userService.createUser(registerParam); @@ -44,6 +73,13 @@ public ResponseEntity createUser(@Valid @RequestBody RegisterParam registerParam .body(userResponse(new UserWithToken(userData, jwtService.toToken(user)))); } + /** + * Authenticates a user and returns a JWT token. + * + * @param loginParam the login credentials + * @return the user data with JWT token + * @throws InvalidAuthenticationException if credentials are invalid + */ @RequestMapping(path = "/users/login", method = POST) public ResponseEntity userLogin(@Valid @RequestBody LoginParam loginParam) { Optional optional = userRepository.findByEmail(loginParam.getEmail()); @@ -57,6 +93,12 @@ public ResponseEntity userLogin(@Valid @RequestBody LoginParam loginParam) { } } + /** + * Wraps user data with token in a response map. + * + * @param userWithToken the user data with token + * @return a map containing the user data + */ private Map userResponse(UserWithToken userWithToken) { return new HashMap() { { @@ -66,14 +108,21 @@ private Map userResponse(UserWithToken userWithToken) { } } +/** + * Parameter object for user login. + * + * @see UsersApi#userLogin(LoginParam) + */ @Getter @JsonRootName("user") @NoArgsConstructor class LoginParam { + /** The user's email address. Must be a valid email format. */ @NotBlank(message = "can't be empty") @Email(message = "should be an email") private String email; + /** The user's password. Must not be blank. */ @NotBlank(message = "can't be empty") private String password; } diff --git a/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java b/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java index ade3ff4ff..d484445bc 100644 --- a/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java +++ b/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java @@ -21,6 +21,16 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +/** + * Global exception handler for REST API controllers. + * + *

This class provides centralized exception handling for all REST controllers, + * converting various exceptions into appropriate HTTP responses with structured + * error messages. + * + * @see ErrorResource + * @see FieldErrorResource + */ @RestControllerAdvice public class CustomizeExceptionHandler extends ResponseEntityExceptionHandler { diff --git a/src/main/java/io/spring/api/exception/ErrorResource.java b/src/main/java/io/spring/api/exception/ErrorResource.java index 1c2780580..0483ac3f4 100644 --- a/src/main/java/io/spring/api/exception/ErrorResource.java +++ b/src/main/java/io/spring/api/exception/ErrorResource.java @@ -5,13 +5,29 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.util.List; +/** + * Resource class representing API error responses. + * + *

This class wraps a collection of field errors for serialization + * in API error responses. It uses a custom serializer to format the + * errors according to the RealWorld API specification. + * + * @see ErrorResourceSerializer + * @see FieldErrorResource + */ @JsonSerialize(using = ErrorResourceSerializer.class) @JsonIgnoreProperties(ignoreUnknown = true) @lombok.Getter @JsonRootName("errors") public class ErrorResource { + /** The list of field-level errors. */ private List fieldErrors; + /** + * Creates a new ErrorResource with the specified field errors. + * + * @param fieldErrorResources the list of field errors + */ public ErrorResource(List fieldErrorResources) { this.fieldErrors = fieldErrorResources; } diff --git a/src/main/java/io/spring/api/exception/ErrorResourceSerializer.java b/src/main/java/io/spring/api/exception/ErrorResourceSerializer.java index 2ce3816a6..2946386f2 100644 --- a/src/main/java/io/spring/api/exception/ErrorResourceSerializer.java +++ b/src/main/java/io/spring/api/exception/ErrorResourceSerializer.java @@ -10,6 +10,23 @@ import java.util.List; import java.util.Map; +/** + * Custom JSON serializer for {@link ErrorResource}. + * + *

This serializer formats error responses according to the RealWorld API + * specification, grouping error messages by field name. + * + *

Output format: + *

+ * {
+ *   "errors": {
+ *     "fieldName": ["error message 1", "error message 2"]
+ *   }
+ * }
+ * 
+ * + * @see ErrorResource + */ public class ErrorResourceSerializer extends JsonSerializer { @Override public void serialize(ErrorResource value, JsonGenerator gen, SerializerProvider serializers) diff --git a/src/main/java/io/spring/api/exception/FieldErrorResource.java b/src/main/java/io/spring/api/exception/FieldErrorResource.java index 13d57314d..30d90871e 100644 --- a/src/main/java/io/spring/api/exception/FieldErrorResource.java +++ b/src/main/java/io/spring/api/exception/FieldErrorResource.java @@ -4,12 +4,27 @@ import lombok.AllArgsConstructor; import lombok.Getter; +/** + * Resource class representing a single field validation error. + * + *

This class contains details about a validation error on a specific field, + * including the resource name, field name, error code, and human-readable message. + * + * @see ErrorResource + */ @JsonIgnoreProperties(ignoreUnknown = true) @Getter @AllArgsConstructor public class FieldErrorResource { + /** The name of the resource/object containing the field. */ private String resource; + + /** The name of the field with the error. */ private String field; + + /** The validation error code. */ private String code; + + /** The human-readable error message. */ private String message; } diff --git a/src/main/java/io/spring/api/exception/InvalidAuthenticationException.java b/src/main/java/io/spring/api/exception/InvalidAuthenticationException.java index 96af7a838..4b3f9f7f5 100644 --- a/src/main/java/io/spring/api/exception/InvalidAuthenticationException.java +++ b/src/main/java/io/spring/api/exception/InvalidAuthenticationException.java @@ -1,7 +1,18 @@ package io.spring.api.exception; +/** + * Exception thrown when user authentication fails. + * + *

This exception is thrown when a user provides invalid credentials + * (email or password) during login. It results in an HTTP 422 response. + * + * @see CustomizeExceptionHandler#handleInvalidAuthentication + */ public class InvalidAuthenticationException extends RuntimeException { + /** + * Creates a new InvalidAuthenticationException with a default message. + */ public InvalidAuthenticationException() { super("invalid email or password"); } diff --git a/src/main/java/io/spring/api/exception/InvalidRequestException.java b/src/main/java/io/spring/api/exception/InvalidRequestException.java index 68b6c868a..4f1cb94aa 100644 --- a/src/main/java/io/spring/api/exception/InvalidRequestException.java +++ b/src/main/java/io/spring/api/exception/InvalidRequestException.java @@ -2,15 +2,35 @@ import org.springframework.validation.Errors; +/** + * Exception thrown when a request contains validation errors. + * + *

This exception wraps Spring's {@link Errors} object containing + * field-level validation errors. It results in an HTTP 422 response + * with detailed error information. + * + * @see CustomizeExceptionHandler#handleInvalidRequest + */ @SuppressWarnings("serial") public class InvalidRequestException extends RuntimeException { + /** The validation errors from the request. */ private final Errors errors; + /** + * Creates a new InvalidRequestException with the specified validation errors. + * + * @param errors the validation errors + */ public InvalidRequestException(Errors errors) { super(""); this.errors = errors; } + /** + * Returns the validation errors. + * + * @return the validation errors + */ public Errors getErrors() { return errors; } diff --git a/src/main/java/io/spring/api/exception/NoAuthorizationException.java b/src/main/java/io/spring/api/exception/NoAuthorizationException.java index 67414233e..9be9ee90c 100644 --- a/src/main/java/io/spring/api/exception/NoAuthorizationException.java +++ b/src/main/java/io/spring/api/exception/NoAuthorizationException.java @@ -3,5 +3,12 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; +/** + * Exception thrown when a user lacks authorization for an operation. + * + *

This exception is thrown when a user attempts to perform an action + * they are not authorized to do (e.g., editing another user's article). + * It results in an HTTP 403 Forbidden response. + */ @ResponseStatus(HttpStatus.FORBIDDEN) public class NoAuthorizationException extends RuntimeException {} diff --git a/src/main/java/io/spring/api/exception/ResourceNotFoundException.java b/src/main/java/io/spring/api/exception/ResourceNotFoundException.java index 8401e5267..039de85dd 100644 --- a/src/main/java/io/spring/api/exception/ResourceNotFoundException.java +++ b/src/main/java/io/spring/api/exception/ResourceNotFoundException.java @@ -3,5 +3,11 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; +/** + * Exception thrown when a requested resource is not found. + * + *

This exception is thrown when a requested entity (article, user, comment, etc.) + * does not exist in the database. It results in an HTTP 404 Not Found response. + */ @ResponseStatus(value = HttpStatus.NOT_FOUND) public class ResourceNotFoundException extends RuntimeException {} diff --git a/src/main/java/io/spring/api/security/JwtTokenFilter.java b/src/main/java/io/spring/api/security/JwtTokenFilter.java index 1b5c50146..ffca86b8f 100644 --- a/src/main/java/io/spring/api/security/JwtTokenFilter.java +++ b/src/main/java/io/spring/api/security/JwtTokenFilter.java @@ -15,12 +15,41 @@ import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.web.filter.OncePerRequestFilter; +/** + * JWT authentication filter for processing Bearer tokens. + * + *

This filter intercepts incoming requests and extracts JWT tokens from the + * Authorization header. If a valid token is found, it authenticates the user + * and sets the security context. + * + *

Token format: "Token {jwt}" or "Bearer {jwt}" + * + * @see JwtService + * @see WebSecurityConfig + */ @SuppressWarnings("SpringJavaAutowiringInspection") public class JwtTokenFilter extends OncePerRequestFilter { + /** Repository for looking up users by ID. */ @Autowired private UserRepository userRepository; + + /** Service for JWT token validation and parsing. */ @Autowired private JwtService jwtService; + + /** The HTTP header name containing the JWT token. */ private final String header = "Authorization"; + /** + * Processes the JWT token from the request and authenticates the user. + * + *

This method extracts the JWT token from the Authorization header, + * validates it, and if valid, sets the authenticated user in the security context. + * + * @param request the HTTP request + * @param response the HTTP response + * @param filterChain the filter chain + * @throws ServletException if a servlet error occurs + * @throws IOException if an I/O error occurs + */ @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -47,6 +76,14 @@ protected void doFilterInternal( filterChain.doFilter(request, response); } + /** + * Extracts the token string from the Authorization header. + * + *

The header is expected to be in the format "Token {jwt}" or "Bearer {jwt}". + * + * @param header the Authorization header value + * @return an Optional containing the token string, or empty if not present + */ private Optional getTokenString(String header) { if (header == null) { return Optional.empty(); diff --git a/src/main/java/io/spring/api/security/WebSecurityConfig.java b/src/main/java/io/spring/api/security/WebSecurityConfig.java index 3786959ef..dcb0cf45e 100644 --- a/src/main/java/io/spring/api/security/WebSecurityConfig.java +++ b/src/main/java/io/spring/api/security/WebSecurityConfig.java @@ -18,20 +18,58 @@ import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +/** + * Spring Security configuration for the application. + * + *

This class configures: + *

    + *
  • JWT-based stateless authentication
  • + *
  • CORS settings for cross-origin requests
  • + *
  • URL-based authorization rules
  • + *
  • Password encoding using BCrypt
  • + *
+ * + * @see JwtTokenFilter + */ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + /** + * Creates the JWT token filter bean. + * + * @return a new JwtTokenFilter instance + */ @Bean public JwtTokenFilter jwtTokenFilter() { return new JwtTokenFilter(); } + /** + * Creates the password encoder bean using BCrypt. + * + * @return a BCryptPasswordEncoder instance + */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + /** + * Configures HTTP security settings. + * + *

This method configures: + *

    + *
  • CSRF disabled (stateless API)
  • + *
  • CORS enabled
  • + *
  • Stateless session management
  • + *
  • Public endpoints: registration, login, article/profile/tag reads
  • + *
  • Authenticated endpoints: all other requests
  • + *
+ * + * @param http the HttpSecurity to configure + * @throws Exception if configuration fails + */ @Override protected void configure(HttpSecurity http) throws Exception { @@ -64,6 +102,18 @@ protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); } + /** + * Configures CORS settings for the application. + * + *

This configuration allows: + *

    + *
  • All origins
  • + *
  • HEAD, GET, POST, PUT, DELETE, PATCH methods
  • + *
  • Authorization, Cache-Control, Content-Type headers
  • + *
+ * + * @return the CORS configuration source + */ @Bean public CorsConfigurationSource corsConfigurationSource() { final CorsConfiguration configuration = new CorsConfiguration(); diff --git a/src/main/java/io/spring/application/ArticleQueryService.java b/src/main/java/io/spring/application/ArticleQueryService.java index 959e8c638..236e40014 100644 --- a/src/main/java/io/spring/application/ArticleQueryService.java +++ b/src/main/java/io/spring/application/ArticleQueryService.java @@ -20,13 +20,43 @@ import org.joda.time.DateTime; import org.springframework.stereotype.Service; +/** + * Service class for querying article data following the CQRS pattern. + * + *

This service is responsible for reading article data and enriching it with + * additional information such as favorite counts, whether the current user has + * favorited the article, and whether the current user is following the author. + * + *

The service supports both offset-based pagination and cursor-based pagination + * for efficient data retrieval in different scenarios. + * + * @see ArticleData + * @see ArticleDataList + * @see CursorPager + */ @Service @AllArgsConstructor public class ArticleQueryService { + /** Service for reading article data from the database. */ private ArticleReadService articleReadService; + + /** Service for querying user relationship data. */ private UserRelationshipQueryService userRelationshipQueryService; + + /** Service for reading article favorite data. */ private ArticleFavoritesReadService articleFavoritesReadService; + /** + * Finds an article by its unique identifier. + * + *

If a user is provided, the article data is enriched with user-specific + * information such as whether the user has favorited the article and whether + * the user is following the author. + * + * @param id the unique identifier of the article + * @param user the current user, or null if not authenticated + * @return an Optional containing the article data if found, or empty if not found + */ public Optional findById(String id, User user) { ArticleData articleData = articleReadService.findById(id); if (articleData == null) { @@ -39,6 +69,17 @@ public Optional findById(String id, User user) { } } + /** + * Finds an article by its URL-friendly slug. + * + *

If a user is provided, the article data is enriched with user-specific + * information such as whether the user has favorited the article and whether + * the user is following the author. + * + * @param slug the URL-friendly slug of the article + * @param user the current user, or null if not authenticated + * @return an Optional containing the article data if found, or empty if not found + */ public Optional findBySlug(String slug, User user) { ArticleData articleData = articleReadService.findBySlug(slug); if (articleData == null) { @@ -51,6 +92,19 @@ public Optional findBySlug(String slug, User user) { } } + /** + * Finds recent articles with cursor-based pagination. + * + *

This method supports filtering by tag, author, and favorited-by user. + * Results are paginated using cursor-based pagination for efficient scrolling. + * + * @param tag filter by tag name, or null for no tag filter + * @param author filter by author username, or null for no author filter + * @param favoritedBy filter by user who favorited, or null for no favorite filter + * @param page cursor pagination parameters + * @param currentUser the current user for enriching data, or null if not authenticated + * @return a CursorPager containing the matching articles + */ public CursorPager findRecentArticlesWithCursor( String tag, String author, @@ -77,6 +131,16 @@ public CursorPager findRecentArticlesWithCursor( } } + /** + * Finds articles from users that the current user follows with cursor-based pagination. + * + *

This method returns articles only from authors that the specified user is following, + * providing a personalized feed experience. + * + * @param user the current user whose feed is being retrieved + * @param page cursor pagination parameters + * @return a CursorPager containing articles from followed users + */ public CursorPager findUserFeedWithCursor( User user, CursorPageParameter page) { List followdUsers = userRelationshipQueryService.followedUsers(user.getId()); @@ -97,6 +161,19 @@ public CursorPager findUserFeedWithCursor( } } + /** + * Finds recent articles with offset-based pagination. + * + *

This method supports filtering by tag, author, and favorited-by user. + * Results are paginated using traditional offset-based pagination. + * + * @param tag filter by tag name, or null for no tag filter + * @param author filter by author username, or null for no author filter + * @param favoritedBy filter by user who favorited, or null for no favorite filter + * @param page offset pagination parameters + * @param currentUser the current user for enriching data, or null if not authenticated + * @return an ArticleDataList containing the matching articles and total count + */ public ArticleDataList findRecentArticles( String tag, String author, String favoritedBy, Page page, User currentUser) { List articleIds = articleReadService.queryArticles(tag, author, favoritedBy, page); @@ -110,6 +187,16 @@ public ArticleDataList findRecentArticles( } } + /** + * Finds articles from users that the current user follows with offset-based pagination. + * + *

This method returns articles only from authors that the specified user is following, + * providing a personalized feed experience with traditional pagination. + * + * @param user the current user whose feed is being retrieved + * @param page offset pagination parameters + * @return an ArticleDataList containing articles from followed users and total count + */ public ArticleDataList findUserFeed(User user, Page page) { List followdUsers = userRelationshipQueryService.followedUsers(user.getId()); if (followdUsers.size() == 0) { @@ -122,6 +209,16 @@ public ArticleDataList findUserFeed(User user, Page page) { } } + /** + * Enriches a list of articles with additional information. + * + *

Sets favorite counts for all articles, and if a user is provided, + * also sets whether each article is favorited by the user and whether + * the user is following each article's author. + * + * @param articles the list of articles to enrich + * @param currentUser the current user, or null if not authenticated + */ private void fillExtraInfo(List articles, User currentUser) { setFavoriteCount(articles); if (currentUser != null) { @@ -130,6 +227,15 @@ private void fillExtraInfo(List articles, User currentUser) { } } + /** + * Sets the following status for each article's author. + * + *

Checks which authors the current user is following and updates + * the profile data accordingly. + * + * @param articles the list of articles to update + * @param currentUser the current user + */ private void setIsFollowingAuthor(List articles, User currentUser) { Set followingAuthors = userRelationshipQueryService.followingAuthors( @@ -145,6 +251,14 @@ private void setIsFollowingAuthor(List articles, User currentUser) }); } + /** + * Sets the favorite count for each article. + * + *

Retrieves the favorite counts for all articles in a single batch query + * and updates each article's favorite count. + * + * @param articles the list of articles to update + */ private void setFavoriteCount(List articles) { List favoritesCounts = articleFavoritesReadService.articlesFavoriteCount( @@ -158,6 +272,15 @@ private void setFavoriteCount(List articles) { articleData -> articleData.setFavoritesCount(countMap.get(articleData.getId()))); } + /** + * Sets the favorited status for each article based on the current user. + * + *

Checks which articles the current user has favorited and updates + * the favorited flag accordingly. + * + * @param articles the list of articles to update + * @param currentUser the current user + */ private void setIsFavorite(List articles, User currentUser) { Set favoritedArticles = articleFavoritesReadService.userFavorites( @@ -172,6 +295,16 @@ private void setIsFavorite(List articles, User currentUser) { }); } + /** + * Enriches a single article with user-specific information. + * + *

Sets whether the user has favorited the article, the total favorite count, + * and whether the user is following the article's author. + * + * @param id the article ID + * @param user the current user + * @param articleData the article data to enrich + */ private void fillExtraInfo(String id, User user, ArticleData articleData) { articleData.setFavorited(articleFavoritesReadService.isUserFavorite(user.getId(), id)); articleData.setFavoritesCount(articleFavoritesReadService.articleFavoriteCount(id)); diff --git a/src/main/java/io/spring/application/CommentQueryService.java b/src/main/java/io/spring/application/CommentQueryService.java index da1677f4c..2847f5e5c 100644 --- a/src/main/java/io/spring/application/CommentQueryService.java +++ b/src/main/java/io/spring/application/CommentQueryService.java @@ -14,12 +14,37 @@ import org.joda.time.DateTime; import org.springframework.stereotype.Service; +/** + * Service class for querying comment data following the CQRS pattern. + * + *

This service is responsible for reading comment data and enriching it with + * additional information such as whether the current user is following the comment author. + * + *

The service supports both simple list retrieval and cursor-based pagination + * for efficient data retrieval in different scenarios. + * + * @see CommentData + * @see CursorPager + */ @Service @AllArgsConstructor public class CommentQueryService { + /** Service for reading comment data from the database. */ private CommentReadService commentReadService; + + /** Service for querying user relationship data. */ private UserRelationshipQueryService userRelationshipQueryService; + /** + * Finds a comment by its unique identifier. + * + *

If a user is provided, the comment data is enriched with information + * about whether the user is following the comment author. + * + * @param id the unique identifier of the comment + * @param user the current user, or null if not authenticated + * @return an Optional containing the comment data if found, or empty if not found + */ public Optional findById(String id, User user) { CommentData commentData = commentReadService.findById(id); if (commentData == null) { @@ -34,6 +59,16 @@ public Optional findById(String id, User user) { return Optional.ofNullable(commentData); } + /** + * Finds all comments for a specific article. + * + *

If a user is provided, the comment data is enriched with information + * about whether the user is following each comment author. + * + * @param articleId the unique identifier of the article + * @param user the current user, or null if not authenticated + * @return a list of comments for the article + */ public List findByArticleId(String articleId, User user) { List comments = commentReadService.findByArticleId(articleId); if (comments.size() > 0 && user != null) { @@ -53,6 +88,18 @@ public List findByArticleId(String articleId, User user) { return comments; } + /** + * Finds comments for a specific article with cursor-based pagination. + * + *

If a user is provided, the comment data is enriched with information + * about whether the user is following each comment author. Results are + * paginated using cursor-based pagination for efficient scrolling. + * + * @param articleId the unique identifier of the article + * @param user the current user, or null if not authenticated + * @param page cursor pagination parameters + * @return a CursorPager containing the matching comments + */ public CursorPager findByArticleIdWithCursor( String articleId, User user, CursorPageParameter page) { List comments = commentReadService.findByArticleIdWithCursor(articleId, page); diff --git a/src/main/java/io/spring/application/CursorPageParameter.java b/src/main/java/io/spring/application/CursorPageParameter.java index 195313736..d0b21cc89 100644 --- a/src/main/java/io/spring/application/CursorPageParameter.java +++ b/src/main/java/io/spring/application/CursorPageParameter.java @@ -4,32 +4,89 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Represents pagination parameters for cursor-based pagination. + * + *

This class encapsulates the cursor, limit, and direction values used for + * cursor-based pagination. It includes validation to ensure values are within + * acceptable ranges. + * + *

Default values: + *

    + *
  • limit: 20 (return 20 items per page)
  • + *
  • cursor: null (start from the beginning)
  • + *
+ * + *

The maximum limit is capped at 1000 to prevent excessive data retrieval. + * + * @param the type of the cursor value (typically DateTime or String) + * @see CursorPager + * @see Page + */ @Data @NoArgsConstructor public class CursorPageParameter { + /** Maximum allowed limit to prevent excessive data retrieval. */ private static final int MAX_LIMIT = 1000; + + /** The maximum number of items to return. */ private int limit = 20; + + /** The cursor marking the position in the dataset. */ private T cursor; + + /** The direction of pagination (NEXT or PREV). */ private Direction direction; + /** + * Creates a new CursorPageParameter with the specified values. + * + *

The limit is validated and capped at MAX_LIMIT if necessary. + * + * @param cursor the cursor marking the position in the dataset + * @param limit the maximum number of items to return + * @param direction the direction of pagination + */ public CursorPageParameter(T cursor, int limit, Direction direction) { setLimit(limit); setCursor(cursor); setDirection(direction); } + /** + * Checks if the pagination direction is NEXT. + * + * @return {@code true} if navigating forward, {@code false} otherwise + */ public boolean isNext() { return direction == Direction.NEXT; } + /** + * Gets the query limit, which is one more than the requested limit. + * + *

The extra item is used to determine if there are more pages available. + * + * @return the limit plus one for pagination detection + */ public int getQueryLimit() { return limit + 1; } + /** + * Sets the cursor value. + * + * @param cursor the cursor value + */ private void setCursor(T cursor) { this.cursor = cursor; } + /** + * Sets the limit value with validation. + * + * @param limit the limit value (capped at MAX_LIMIT, must be positive) + */ private void setLimit(int limit) { if (limit > MAX_LIMIT) { this.limit = MAX_LIMIT; diff --git a/src/main/java/io/spring/application/CursorPager.java b/src/main/java/io/spring/application/CursorPager.java index 13d55d4cd..17b5d3f23 100644 --- a/src/main/java/io/spring/application/CursorPager.java +++ b/src/main/java/io/spring/application/CursorPager.java @@ -3,12 +3,43 @@ import java.util.List; import lombok.Getter; +/** + * A generic cursor-based pagination container. + * + *

This class implements cursor-based pagination (also known as keyset pagination), + * which is more efficient than offset-based pagination for large datasets. It provides + * information about whether there are more pages available in either direction. + * + *

Cursor-based pagination uses a cursor (typically a timestamp or ID) to mark + * the position in the dataset, allowing for efficient navigation without the + * performance issues of large offsets. + * + * @param the type of elements in the page, must extend {@link Node} + * @see Node + * @see PageCursor + * @see CursorPageParameter + */ @Getter public class CursorPager { + /** The list of items in the current page. */ private List data; + + /** Whether there are more items after the current page. */ private boolean next; + + /** Whether there are more items before the current page. */ private boolean previous; + /** + * Creates a new CursorPager with the specified data and pagination state. + * + *

The hasExtra parameter indicates whether there are more items beyond + * the current page in the direction of navigation. + * + * @param data the list of items in the current page + * @param direction the direction of pagination (NEXT or PREV) + * @param hasExtra whether there are more items in the navigation direction + */ public CursorPager(List data, Direction direction, boolean hasExtra) { this.data = data; @@ -21,24 +52,49 @@ public CursorPager(List data, Direction direction, boolean hasExtra) { } } + /** + * Checks if there are more items after the current page. + * + * @return {@code true} if there are more items, {@code false} otherwise + */ public boolean hasNext() { return next; } + /** + * Checks if there are more items before the current page. + * + * @return {@code true} if there are more items, {@code false} otherwise + */ public boolean hasPrevious() { return previous; } + /** + * Gets the cursor for the first item in the current page. + * + * @return the start cursor, or null if the page is empty + */ public PageCursor getStartCursor() { return data.isEmpty() ? null : data.get(0).getCursor(); } + /** + * Gets the cursor for the last item in the current page. + * + * @return the end cursor, or null if the page is empty + */ public PageCursor getEndCursor() { return data.isEmpty() ? null : data.get(data.size() - 1).getCursor(); } + /** + * Enumeration of pagination directions. + */ public enum Direction { + /** Navigate to previous items. */ PREV, + /** Navigate to next items. */ NEXT } } diff --git a/src/main/java/io/spring/application/DateTimeCursor.java b/src/main/java/io/spring/application/DateTimeCursor.java index cfcc86bc8..cc8fec4a4 100644 --- a/src/main/java/io/spring/application/DateTimeCursor.java +++ b/src/main/java/io/spring/application/DateTimeCursor.java @@ -3,17 +3,49 @@ import org.joda.time.DateTime; import org.joda.time.DateTimeZone; +/** + * A cursor implementation for DateTime-based pagination. + * + *

This cursor uses Joda-Time DateTime values to mark positions in datasets + * that are ordered by timestamp. The cursor is serialized as milliseconds since + * epoch for efficient storage and transmission. + * + * @see PageCursor + * @see CursorPager + */ public class DateTimeCursor extends PageCursor { + /** + * Creates a new DateTimeCursor with the specified DateTime. + * + * @param data the DateTime value for the cursor + */ public DateTimeCursor(DateTime data) { super(data); } + /** + * Returns the string representation of this cursor. + * + *

The DateTime is serialized as milliseconds since epoch for efficient + * storage and transmission. + * + * @return the milliseconds since epoch as a string + */ @Override public String toString() { return String.valueOf(getData().getMillis()); } + /** + * Parses a cursor string into a DateTime. + * + *

The cursor string should be milliseconds since epoch. The resulting + * DateTime is set to UTC timezone. + * + * @param cursor the cursor string to parse, or null + * @return the parsed DateTime in UTC, or null if the cursor is null + */ public static DateTime parse(String cursor) { if (cursor == null) { return null; diff --git a/src/main/java/io/spring/application/Node.java b/src/main/java/io/spring/application/Node.java index e4ccac8a6..3bff8b320 100644 --- a/src/main/java/io/spring/application/Node.java +++ b/src/main/java/io/spring/application/Node.java @@ -1,5 +1,23 @@ package io.spring.application; +/** + * Interface for entities that support cursor-based pagination. + * + *

Classes implementing this interface can be used with {@link CursorPager} + * for cursor-based pagination. Each node must provide a cursor that uniquely + * identifies its position in the dataset. + * + * @see CursorPager + * @see PageCursor + */ public interface Node { + /** + * Gets the cursor for this node. + * + *

The cursor is used to identify the position of this item in the dataset + * for pagination purposes. + * + * @return the cursor for this node + */ PageCursor getCursor(); } diff --git a/src/main/java/io/spring/application/Page.java b/src/main/java/io/spring/application/Page.java index d273e994f..ba3aa0996 100644 --- a/src/main/java/io/spring/application/Page.java +++ b/src/main/java/io/spring/application/Page.java @@ -3,24 +3,69 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Represents pagination parameters for offset-based pagination. + * + *

This class encapsulates the offset and limit values used for traditional + * offset-based pagination. It includes validation to ensure values are within + * acceptable ranges. + * + *

Default values: + *

    + *
  • offset: 0 (start from the beginning)
  • + *
  • limit: 20 (return 20 items per page)
  • + *
+ * + *

The maximum limit is capped at 100 to prevent excessive data retrieval. + * + * @see CursorPageParameter + */ @NoArgsConstructor @Data public class Page { + /** Maximum allowed limit to prevent excessive data retrieval. */ private static final int MAX_LIMIT = 100; + + /** The number of items to skip from the beginning. */ private int offset = 0; + + /** The maximum number of items to return. */ private int limit = 20; + /** + * Creates a new Page with the specified offset and limit. + * + *

Values are validated and adjusted if necessary: + *

    + *
  • Negative offsets are ignored (defaults to 0)
  • + *
  • Limits exceeding MAX_LIMIT are capped at MAX_LIMIT
  • + *
  • Non-positive limits are ignored (defaults to 20)
  • + *
+ * + * @param offset the number of items to skip + * @param limit the maximum number of items to return + */ public Page(int offset, int limit) { setOffset(offset); setLimit(limit); } + /** + * Sets the offset value with validation. + * + * @param offset the offset value (must be positive to take effect) + */ private void setOffset(int offset) { if (offset > 0) { this.offset = offset; } } + /** + * Sets the limit value with validation. + * + * @param limit the limit value (capped at MAX_LIMIT, must be positive) + */ private void setLimit(int limit) { if (limit > MAX_LIMIT) { this.limit = MAX_LIMIT; diff --git a/src/main/java/io/spring/application/PageCursor.java b/src/main/java/io/spring/application/PageCursor.java index 0279f3b20..06cde095f 100644 --- a/src/main/java/io/spring/application/PageCursor.java +++ b/src/main/java/io/spring/application/PageCursor.java @@ -1,16 +1,44 @@ package io.spring.application; +/** + * Abstract base class for pagination cursors. + * + *

A cursor is a marker that identifies a specific position in a dataset, + * used for cursor-based pagination. Subclasses implement specific cursor types + * for different data types (e.g., DateTime, String). + * + * @param the type of data stored in the cursor + * @see DateTimeCursor + * @see CursorPager + * @see Node + */ public abstract class PageCursor { + /** The data value that identifies the position in the dataset. */ private T data; + /** + * Creates a new PageCursor with the specified data. + * + * @param data the data value for the cursor + */ public PageCursor(T data) { this.data = data; } + /** + * Gets the data value of this cursor. + * + * @return the cursor data + */ public T getData() { return data; } + /** + * Returns a string representation of this cursor. + * + * @return the string representation of the cursor data + */ @Override public String toString() { return data.toString(); diff --git a/src/main/java/io/spring/application/ProfileQueryService.java b/src/main/java/io/spring/application/ProfileQueryService.java index d92542d5b..08f60bafc 100644 --- a/src/main/java/io/spring/application/ProfileQueryService.java +++ b/src/main/java/io/spring/application/ProfileQueryService.java @@ -9,12 +9,34 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; +/** + * Service class for querying user profile data following the CQRS pattern. + * + *

This service is responsible for reading user profile data and enriching it + * with information about whether the current user is following the profile owner. + * + * @see ProfileData + * @see UserData + */ @Component @AllArgsConstructor public class ProfileQueryService { + /** Service for reading user data from the database. */ private UserReadService userReadService; + + /** Service for querying user relationship data. */ private UserRelationshipQueryService userRelationshipQueryService; + /** + * Finds a user profile by username. + * + *

If a current user is provided, the profile data is enriched with information + * about whether the current user is following the profile owner. + * + * @param username the username to search for + * @param currentUser the current user, or null if not authenticated + * @return an Optional containing the profile data if found, or empty if not found + */ public Optional findByUsername(String username, User currentUser) { UserData userData = userReadService.findByUsername(username); if (userData == null) { diff --git a/src/main/java/io/spring/application/TagsQueryService.java b/src/main/java/io/spring/application/TagsQueryService.java index 12e0790cb..53da73bed 100644 --- a/src/main/java/io/spring/application/TagsQueryService.java +++ b/src/main/java/io/spring/application/TagsQueryService.java @@ -5,11 +5,25 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; +/** + * Service class for querying tag data following the CQRS pattern. + * + *

This service is responsible for reading tag data from the database. + * It provides a simple interface for retrieving all available tags. + * + * @see io.spring.core.article.Tag + */ @Service @AllArgsConstructor public class TagsQueryService { + /** Service for reading tag data from the database. */ private TagReadService tagReadService; + /** + * Retrieves all available tags. + * + * @return a list of all tag names + */ public List allTags() { return tagReadService.all(); } diff --git a/src/main/java/io/spring/application/UserQueryService.java b/src/main/java/io/spring/application/UserQueryService.java index f0f901ae2..41678f97c 100644 --- a/src/main/java/io/spring/application/UserQueryService.java +++ b/src/main/java/io/spring/application/UserQueryService.java @@ -6,11 +6,26 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; +/** + * Service class for querying user data following the CQRS pattern. + * + *

This service is responsible for reading user data from the database. + * It provides a simple interface for retrieving user information by ID. + * + * @see UserData + */ @Service @AllArgsConstructor public class UserQueryService { + /** Service for reading user data from the database. */ private UserReadService userReadService; + /** + * Finds a user by their unique identifier. + * + * @param id the unique identifier of the user + * @return an Optional containing the user data if found, or empty if not found + */ public Optional findById(String id) { return Optional.ofNullable(userReadService.findById(id)); } diff --git a/src/main/java/io/spring/application/article/ArticleCommandService.java b/src/main/java/io/spring/application/article/ArticleCommandService.java index 861bc22b2..ce2dde2be 100644 --- a/src/main/java/io/spring/application/article/ArticleCommandService.java +++ b/src/main/java/io/spring/application/article/ArticleCommandService.java @@ -8,13 +8,34 @@ import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +/** + * Service class for article write operations following the CQRS pattern. + * + *

This service handles the command side of article operations, including + * creating new articles and updating existing ones. It validates input parameters + * using Bean Validation annotations. + * + * @see io.spring.application.ArticleQueryService + * @see Article + */ @Service @Validated @AllArgsConstructor public class ArticleCommandService { + /** Repository for persisting article entities. */ private ArticleRepository articleRepository; + /** + * Creates a new article. + * + *

The article title is validated to ensure it's not blank and doesn't + * duplicate an existing article's slug. + * + * @param newArticleParam the parameters for the new article + * @param creator the user creating the article + * @return the newly created article + */ public Article createArticle(@Valid NewArticleParam newArticleParam, User creator) { Article article = new Article( @@ -27,6 +48,15 @@ public Article createArticle(@Valid NewArticleParam newArticleParam, User creato return article; } + /** + * Updates an existing article. + * + *

Only non-empty fields in the update parameters will be applied to the article. + * + * @param article the article to update + * @param updateArticleParam the parameters for updating the article + * @return the updated article + */ public Article updateArticle(Article article, @Valid UpdateArticleParam updateArticleParam) { article.update( updateArticleParam.getTitle(), diff --git a/src/main/java/io/spring/application/article/DuplicatedArticleConstraint.java b/src/main/java/io/spring/application/article/DuplicatedArticleConstraint.java index 0eb1eda84..d7d491710 100644 --- a/src/main/java/io/spring/application/article/DuplicatedArticleConstraint.java +++ b/src/main/java/io/spring/application/article/DuplicatedArticleConstraint.java @@ -8,14 +8,38 @@ import javax.validation.Constraint; import javax.validation.Payload; +/** + * Validation constraint annotation for checking duplicate article titles. + * + *

This constraint ensures that an article title doesn't result in a slug + * that already exists in the database. It's used during article creation + * to prevent duplicate articles. + * + * @see DuplicatedArticleValidator + */ @Documented @Constraint(validatedBy = DuplicatedArticleValidator.class) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) public @interface DuplicatedArticleConstraint { + /** + * The error message to display when validation fails. + * + * @return the error message + */ String message() default "article name exists"; + /** + * Groups for constraint categorization. + * + * @return the validation groups + */ Class[] groups() default {}; + /** + * Payload for extensibility purposes. + * + * @return the payload classes + */ Class[] payload() default {}; } diff --git a/src/main/java/io/spring/application/article/DuplicatedArticleValidator.java b/src/main/java/io/spring/application/article/DuplicatedArticleValidator.java index 0e3828e1e..48cad49de 100644 --- a/src/main/java/io/spring/application/article/DuplicatedArticleValidator.java +++ b/src/main/java/io/spring/application/article/DuplicatedArticleValidator.java @@ -6,11 +6,28 @@ import javax.validation.ConstraintValidatorContext; import org.springframework.beans.factory.annotation.Autowired; +/** + * Validator implementation for the {@link DuplicatedArticleConstraint}. + * + *

This validator checks if an article title would result in a slug that + * already exists in the database. It converts the title to a slug and queries + * the database to check for duplicates. + * + * @see DuplicatedArticleConstraint + */ class DuplicatedArticleValidator implements ConstraintValidator { + /** Service for querying article data. */ @Autowired private ArticleQueryService articleQueryService; + /** + * Validates that the article title doesn't result in a duplicate slug. + * + * @param value the article title to validate + * @param context the constraint validator context + * @return {@code true} if the slug doesn't exist, {@code false} otherwise + */ @Override public boolean isValid(String value, ConstraintValidatorContext context) { return !articleQueryService.findBySlug(Article.toSlug(value), null).isPresent(); diff --git a/src/main/java/io/spring/application/article/NewArticleParam.java b/src/main/java/io/spring/application/article/NewArticleParam.java index 344d76c33..6f901f3f6 100644 --- a/src/main/java/io/spring/application/article/NewArticleParam.java +++ b/src/main/java/io/spring/application/article/NewArticleParam.java @@ -8,21 +8,36 @@ import lombok.Getter; import lombok.NoArgsConstructor; +/** + * Parameter object for creating a new article. + * + *

This class represents the input data required to create a new article, + * including validation constraints to ensure data integrity. + * + * @see ArticleCommandService#createArticle(NewArticleParam, io.spring.core.user.User) + */ @Getter @JsonRootName("article") @NoArgsConstructor @AllArgsConstructor @Builder public class NewArticleParam { + /** + * The title of the article. + * Must not be blank and must not duplicate an existing article's slug. + */ @NotBlank(message = "can't be empty") @DuplicatedArticleConstraint private String title; + /** A brief description of the article. Must not be blank. */ @NotBlank(message = "can't be empty") private String description; + /** The main content of the article. Must not be blank. */ @NotBlank(message = "can't be empty") private String body; + /** Optional list of tags to associate with the article. */ private List tagList; } diff --git a/src/main/java/io/spring/application/article/UpdateArticleParam.java b/src/main/java/io/spring/application/article/UpdateArticleParam.java index 14d551439..5bc6d54f3 100644 --- a/src/main/java/io/spring/application/article/UpdateArticleParam.java +++ b/src/main/java/io/spring/application/article/UpdateArticleParam.java @@ -5,12 +5,25 @@ import lombok.Getter; import lombok.NoArgsConstructor; +/** + * Parameter object for updating an existing article. + * + *

This class represents the input data for updating an article. + * All fields are optional - only non-empty values will be applied. + * + * @see ArticleCommandService#updateArticle(io.spring.core.article.Article, UpdateArticleParam) + */ @Getter @NoArgsConstructor @AllArgsConstructor @JsonRootName("article") public class UpdateArticleParam { + /** The new title for the article. Empty string means no change. */ private String title = ""; + + /** The new body content for the article. Empty string means no change. */ private String body = ""; + + /** The new description for the article. Empty string means no change. */ private String description = ""; } diff --git a/src/main/java/io/spring/application/data/ArticleData.java b/src/main/java/io/spring/application/data/ArticleData.java index 3d3c947e2..39ceaebc6 100644 --- a/src/main/java/io/spring/application/data/ArticleData.java +++ b/src/main/java/io/spring/application/data/ArticleData.java @@ -8,24 +8,64 @@ import lombok.NoArgsConstructor; import org.joda.time.DateTime; +/** + * Data Transfer Object (DTO) for article information. + * + *

This class represents article data as returned by the API, including + * the article content, metadata, author profile, and user-specific information + * such as whether the current user has favorited the article. + * + *

Implements {@link io.spring.application.Node} to support cursor-based + * pagination using the article's update timestamp. + * + * @see ProfileData + * @see io.spring.core.article.Article + */ @Data @NoArgsConstructor @AllArgsConstructor public class ArticleData implements io.spring.application.Node { + /** The unique identifier of the article. */ private String id; + + /** The URL-friendly slug of the article. */ private String slug; + + /** The title of the article. */ private String title; + + /** A brief description of the article. */ private String description; + + /** The main content of the article. */ private String body; + + /** Whether the current user has favorited this article. */ private boolean favorited; + + /** The total number of users who have favorited this article. */ private int favoritesCount; + + /** The timestamp when the article was created. */ private DateTime createdAt; + + /** The timestamp when the article was last updated. */ private DateTime updatedAt; + + /** The list of tags associated with this article. */ private List tagList; + /** The profile data of the article's author. */ @JsonProperty("author") private ProfileData profileData; + /** + * Gets the cursor for this article for pagination purposes. + * + *

Uses the article's update timestamp as the cursor value. + * + * @return a DateTimeCursor based on the article's update timestamp + */ @Override public DateTimeCursor getCursor() { return new DateTimeCursor(updatedAt); diff --git a/src/main/java/io/spring/application/data/ArticleDataList.java b/src/main/java/io/spring/application/data/ArticleDataList.java index e7ff45fe8..4f598d897 100644 --- a/src/main/java/io/spring/application/data/ArticleDataList.java +++ b/src/main/java/io/spring/application/data/ArticleDataList.java @@ -4,14 +4,30 @@ import java.util.List; import lombok.Getter; +/** + * Data Transfer Object (DTO) for a paginated list of articles. + * + *

This class wraps a list of articles along with the total count, + * used for offset-based pagination responses in the REST API. + * + * @see ArticleData + */ @Getter public class ArticleDataList { + /** The list of articles in the current page. */ @JsonProperty("articles") private final List articleDatas; + /** The total count of articles matching the query. */ @JsonProperty("articlesCount") private final int count; + /** + * Creates a new ArticleDataList with the specified articles and count. + * + * @param articleDatas the list of articles in the current page + * @param count the total count of articles matching the query + */ public ArticleDataList(List articleDatas, int count) { this.articleDatas = articleDatas; diff --git a/src/main/java/io/spring/application/data/ArticleFavoriteCount.java b/src/main/java/io/spring/application/data/ArticleFavoriteCount.java index 1512d422f..35bdb584b 100644 --- a/src/main/java/io/spring/application/data/ArticleFavoriteCount.java +++ b/src/main/java/io/spring/application/data/ArticleFavoriteCount.java @@ -2,8 +2,19 @@ import lombok.Value; +/** + * Data Transfer Object (DTO) for article favorite counts. + * + *

This immutable class represents the favorite count for a specific article, + * used for batch retrieval of favorite counts in the query service. + * + * @see io.spring.application.ArticleQueryService + */ @Value public class ArticleFavoriteCount { + /** The unique identifier of the article. */ private String id; + + /** The number of users who have favorited this article. */ private Integer count; } diff --git a/src/main/java/io/spring/application/data/CommentData.java b/src/main/java/io/spring/application/data/CommentData.java index 1e28d94bd..0b03c9984 100644 --- a/src/main/java/io/spring/application/data/CommentData.java +++ b/src/main/java/io/spring/application/data/CommentData.java @@ -9,19 +9,48 @@ import lombok.NoArgsConstructor; import org.joda.time.DateTime; +/** + * Data Transfer Object (DTO) for comment information. + * + *

This class represents comment data as returned by the API, including + * the comment content, timestamps, and author profile information. + * + *

Implements {@link Node} to support cursor-based pagination using + * the comment's creation timestamp. + * + * @see ProfileData + * @see io.spring.core.comment.Comment + */ @Data @NoArgsConstructor @AllArgsConstructor public class CommentData implements Node { + /** The unique identifier of the comment. */ private String id; + + /** The content of the comment. */ private String body; + + /** The ID of the article this comment belongs to (not included in JSON). */ @JsonIgnore private String articleId; + + /** The timestamp when the comment was created. */ private DateTime createdAt; + + /** The timestamp when the comment was last updated. */ private DateTime updatedAt; + /** The profile data of the comment's author. */ @JsonProperty("author") private ProfileData profileData; + /** + * Gets the cursor for this comment for pagination purposes. + * + *

Uses the comment's creation timestamp as the cursor value. + * + * @return a DateTimeCursor based on the comment's creation timestamp + */ @Override public DateTimeCursor getCursor() { return new DateTimeCursor(createdAt); diff --git a/src/main/java/io/spring/application/data/ProfileData.java b/src/main/java/io/spring/application/data/ProfileData.java index 82ef5f959..d9309169c 100644 --- a/src/main/java/io/spring/application/data/ProfileData.java +++ b/src/main/java/io/spring/application/data/ProfileData.java @@ -5,13 +5,31 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Data Transfer Object (DTO) for user profile information. + * + *

This class represents a user's public profile as returned by the API, + * including their username, bio, profile image, and whether the current user + * is following them. + * + * @see io.spring.core.user.User + */ @Data @NoArgsConstructor @AllArgsConstructor public class ProfileData { + /** The unique identifier of the user (not included in JSON). */ @JsonIgnore private String id; + + /** The user's public username. */ private String username; + + /** The user's biography or description. */ private String bio; + + /** The URL to the user's profile image. */ private String image; + + /** Whether the current user is following this profile. */ private boolean following; } diff --git a/src/main/java/io/spring/application/data/UserData.java b/src/main/java/io/spring/application/data/UserData.java index c50cc190a..fa7894101 100644 --- a/src/main/java/io/spring/application/data/UserData.java +++ b/src/main/java/io/spring/application/data/UserData.java @@ -4,13 +4,32 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Data Transfer Object (DTO) for user information. + * + *

This class represents user data as stored in the database and used + * internally by the application. It contains all user profile information + * except for sensitive data like passwords. + * + * @see io.spring.core.user.User + * @see UserWithToken + */ @Data @NoArgsConstructor @AllArgsConstructor public class UserData { + /** The unique identifier of the user. */ private String id; + + /** The user's email address. */ private String email; + + /** The user's public username. */ private String username; + + /** The user's biography or description. */ private String bio; + + /** The URL to the user's profile image. */ private String image; } diff --git a/src/main/java/io/spring/application/data/UserWithToken.java b/src/main/java/io/spring/application/data/UserWithToken.java index eac7f1b6b..122e660fd 100644 --- a/src/main/java/io/spring/application/data/UserWithToken.java +++ b/src/main/java/io/spring/application/data/UserWithToken.java @@ -2,14 +2,38 @@ import lombok.Getter; +/** + * Data Transfer Object (DTO) for user information with authentication token. + * + *

This class combines user profile data with a JWT authentication token, + * used in API responses after successful login or registration. + * + * @see UserData + * @see io.spring.core.service.JwtService + */ @Getter public class UserWithToken { + /** The user's email address. */ private String email; + + /** The user's public username. */ private String username; + + /** The user's biography or description. */ private String bio; + + /** The URL to the user's profile image. */ private String image; + + /** The JWT authentication token for the user. */ private String token; + /** + * Creates a new UserWithToken from user data and a token. + * + * @param userData the user's profile data + * @param token the JWT authentication token + */ public UserWithToken(UserData userData, String token) { this.email = userData.getEmail(); this.username = userData.getUsername(); diff --git a/src/main/java/io/spring/application/user/DuplicatedEmailConstraint.java b/src/main/java/io/spring/application/user/DuplicatedEmailConstraint.java index e41eb009e..4e4b0d275 100644 --- a/src/main/java/io/spring/application/user/DuplicatedEmailConstraint.java +++ b/src/main/java/io/spring/application/user/DuplicatedEmailConstraint.java @@ -5,12 +5,35 @@ import javax.validation.Constraint; import javax.validation.Payload; +/** + * Validation constraint annotation for checking duplicate email addresses. + * + *

This constraint ensures that an email address is not already registered + * in the database. It's used during user registration to prevent duplicate accounts. + * + * @see DuplicatedEmailValidator + */ @Constraint(validatedBy = DuplicatedEmailValidator.class) @Retention(RetentionPolicy.RUNTIME) public @interface DuplicatedEmailConstraint { + /** + * The error message to display when validation fails. + * + * @return the error message + */ String message() default "duplicated email"; + /** + * Groups for constraint categorization. + * + * @return the validation groups + */ Class[] groups() default {}; + /** + * Payload for extensibility purposes. + * + * @return the payload classes + */ Class[] payload() default {}; } diff --git a/src/main/java/io/spring/application/user/DuplicatedEmailValidator.java b/src/main/java/io/spring/application/user/DuplicatedEmailValidator.java index e30711465..352f9848a 100644 --- a/src/main/java/io/spring/application/user/DuplicatedEmailValidator.java +++ b/src/main/java/io/spring/application/user/DuplicatedEmailValidator.java @@ -5,11 +5,27 @@ import javax.validation.ConstraintValidatorContext; import org.springframework.beans.factory.annotation.Autowired; +/** + * Validator implementation for the {@link DuplicatedEmailConstraint}. + * + *

This validator checks if an email address is already registered in the database. + * Null or empty values are considered valid (other constraints handle required fields). + * + * @see DuplicatedEmailConstraint + */ public class DuplicatedEmailValidator implements ConstraintValidator { + /** Repository for querying existing users. */ @Autowired private UserRepository userRepository; + /** + * Validates that the email address is not already registered. + * + * @param value the email address to validate + * @param context the constraint validator context + * @return {@code true} if the email is not registered or is empty, {@code false} otherwise + */ @Override public boolean isValid(String value, ConstraintValidatorContext context) { return (value == null || value.isEmpty()) || !userRepository.findByEmail(value).isPresent(); diff --git a/src/main/java/io/spring/application/user/DuplicatedUsernameConstraint.java b/src/main/java/io/spring/application/user/DuplicatedUsernameConstraint.java index 4f365b789..7f172d375 100644 --- a/src/main/java/io/spring/application/user/DuplicatedUsernameConstraint.java +++ b/src/main/java/io/spring/application/user/DuplicatedUsernameConstraint.java @@ -5,12 +5,35 @@ import javax.validation.Constraint; import javax.validation.Payload; +/** + * Validation constraint annotation for checking duplicate usernames. + * + *

This constraint ensures that a username is not already taken + * in the database. It's used during user registration to prevent duplicate usernames. + * + * @see DuplicatedUsernameValidator + */ @Constraint(validatedBy = DuplicatedUsernameValidator.class) @Retention(RetentionPolicy.RUNTIME) @interface DuplicatedUsernameConstraint { + /** + * The error message to display when validation fails. + * + * @return the error message + */ String message() default "duplicated username"; + /** + * Groups for constraint categorization. + * + * @return the validation groups + */ Class[] groups() default {}; + /** + * Payload for extensibility purposes. + * + * @return the payload classes + */ Class[] payload() default {}; } diff --git a/src/main/java/io/spring/application/user/DuplicatedUsernameValidator.java b/src/main/java/io/spring/application/user/DuplicatedUsernameValidator.java index ae1fd21aa..15dd4a94c 100644 --- a/src/main/java/io/spring/application/user/DuplicatedUsernameValidator.java +++ b/src/main/java/io/spring/application/user/DuplicatedUsernameValidator.java @@ -5,11 +5,27 @@ import javax.validation.ConstraintValidatorContext; import org.springframework.beans.factory.annotation.Autowired; +/** + * Validator implementation for the {@link DuplicatedUsernameConstraint}. + * + *

This validator checks if a username is already taken in the database. + * Null or empty values are considered valid (other constraints handle required fields). + * + * @see DuplicatedUsernameConstraint + */ class DuplicatedUsernameValidator implements ConstraintValidator { + /** Repository for querying existing users. */ @Autowired private UserRepository userRepository; + /** + * Validates that the username is not already taken. + * + * @param value the username to validate + * @param context the constraint validator context + * @return {@code true} if the username is not taken or is empty, {@code false} otherwise + */ @Override public boolean isValid(String value, ConstraintValidatorContext context) { return (value == null || value.isEmpty()) || !userRepository.findByUsername(value).isPresent(); diff --git a/src/main/java/io/spring/application/user/RegisterParam.java b/src/main/java/io/spring/application/user/RegisterParam.java index 3ba1234d3..fead95fbb 100644 --- a/src/main/java/io/spring/application/user/RegisterParam.java +++ b/src/main/java/io/spring/application/user/RegisterParam.java @@ -7,20 +7,37 @@ import lombok.Getter; import lombok.NoArgsConstructor; +/** + * Parameter object for user registration. + * + *

This class represents the input data required to register a new user, + * including validation constraints to ensure data integrity and uniqueness. + * + * @see UserService#createUser(RegisterParam) + */ @Getter @JsonRootName("user") @AllArgsConstructor @NoArgsConstructor public class RegisterParam { + /** + * The user's email address. + * Must be a valid email format and not already registered. + */ @NotBlank(message = "can't be empty") @Email(message = "should be an email") @DuplicatedEmailConstraint private String email; + /** + * The user's chosen username. + * Must not be blank and not already taken. + */ @NotBlank(message = "can't be empty") @DuplicatedUsernameConstraint private String username; + /** The user's password. Must not be blank. */ @NotBlank(message = "can't be empty") private String password; } diff --git a/src/main/java/io/spring/application/user/UpdateUserCommand.java b/src/main/java/io/spring/application/user/UpdateUserCommand.java index 9df523010..ea257bcda 100644 --- a/src/main/java/io/spring/application/user/UpdateUserCommand.java +++ b/src/main/java/io/spring/application/user/UpdateUserCommand.java @@ -4,11 +4,23 @@ import lombok.AllArgsConstructor; import lombok.Getter; +/** + * Command object for updating a user's profile. + * + *

This class combines the target user with the update parameters, + * allowing validation to check for conflicts with other users. + * + * @see UserService#updateUser(UpdateUserCommand) + * @see UpdateUserParam + */ @Getter @AllArgsConstructor @UpdateUserConstraint public class UpdateUserCommand { + /** The user to be updated. */ private User targetUser; + + /** The parameters containing the new values. */ private UpdateUserParam param; } diff --git a/src/main/java/io/spring/application/user/UpdateUserParam.java b/src/main/java/io/spring/application/user/UpdateUserParam.java index 54cd77471..426783e88 100644 --- a/src/main/java/io/spring/application/user/UpdateUserParam.java +++ b/src/main/java/io/spring/application/user/UpdateUserParam.java @@ -7,6 +7,15 @@ import lombok.Getter; import lombok.NoArgsConstructor; +/** + * Parameter object for updating user profile information. + * + *

This class represents the input data for updating a user's profile. + * All fields are optional - only non-empty values will be applied. + * + * @see UserService#updateUser(UpdateUserCommand) + * @see UpdateUserCommand + */ @Getter @JsonRootName("user") @NoArgsConstructor @@ -14,12 +23,20 @@ @Builder public class UpdateUserParam { + /** The new email address. Empty string means no change. */ @Builder.Default @Email(message = "should be an email") private String email = ""; + /** The new password. Empty string means no change. */ @Builder.Default private String password = ""; + + /** The new username. Empty string means no change. */ @Builder.Default private String username = ""; + + /** The new biography. Empty string means no change. */ @Builder.Default private String bio = ""; + + /** The new profile image URL. Empty string means no change. */ @Builder.Default private String image = ""; } diff --git a/src/main/java/io/spring/application/user/UserService.java b/src/main/java/io/spring/application/user/UserService.java index 48c6735b8..1519b4d71 100644 --- a/src/main/java/io/spring/application/user/UserService.java +++ b/src/main/java/io/spring/application/user/UserService.java @@ -14,13 +14,34 @@ import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +/** + * Service class for user write operations. + * + *

This service handles user registration and profile updates, including + * password encoding and validation of unique constraints for email and username. + * + * @see io.spring.application.UserQueryService + * @see User + */ @Service @Validated public class UserService { + /** Repository for persisting user entities. */ private UserRepository userRepository; + + /** Default profile image URL for new users. */ private String defaultImage; + + /** Encoder for hashing user passwords. */ private PasswordEncoder passwordEncoder; + /** + * Creates a new UserService with the required dependencies. + * + * @param userRepository the repository for user persistence + * @param defaultImage the default profile image URL + * @param passwordEncoder the password encoder for hashing passwords + */ @Autowired public UserService( UserRepository userRepository, @@ -31,6 +52,15 @@ public UserService( this.passwordEncoder = passwordEncoder; } + /** + * Creates a new user account. + * + *

The password is encoded before storage, and the user is assigned + * the default profile image. + * + * @param registerParam the registration parameters + * @return the newly created user + */ public User createUser(@Valid RegisterParam registerParam) { User user = new User( @@ -43,6 +73,13 @@ public User createUser(@Valid RegisterParam registerParam) { return user; } + /** + * Updates an existing user's profile. + * + *

Only non-empty fields in the update parameters will be applied. + * + * @param command the update command containing the target user and parameters + */ public void updateUser(@Valid UpdateUserCommand command) { User user = command.getTargetUser(); UpdateUserParam updateUserParam = command.getParam(); @@ -56,21 +93,60 @@ public void updateUser(@Valid UpdateUserCommand command) { } } +/** + * Validation constraint annotation for user update operations. + * + *

This constraint ensures that email and username updates don't conflict + * with existing users (other than the user being updated). + * + * @see UpdateUserValidator + */ @Constraint(validatedBy = UpdateUserValidator.class) @Retention(RetentionPolicy.RUNTIME) @interface UpdateUserConstraint { - + /** + * The error message to display when validation fails. + * + * @return the error message + */ String message() default "invalid update param"; + /** + * Groups for constraint categorization. + * + * @return the validation groups + */ Class[] groups() default {}; + /** + * Payload for extensibility purposes. + * + * @return the payload classes + */ Class[] payload() default {}; } +/** + * Validator implementation for the {@link UpdateUserConstraint}. + * + *

This validator checks that email and username updates don't conflict + * with other existing users. It allows the user to keep their current + * email or username. + * + * @see UpdateUserConstraint + */ class UpdateUserValidator implements ConstraintValidator { + /** Repository for querying existing users. */ @Autowired private UserRepository userRepository; + /** + * Validates that the email and username updates don't conflict with other users. + * + * @param value the update command to validate + * @param context the constraint validator context + * @return {@code true} if the update is valid, {@code false} otherwise + */ @Override public boolean isValid(UpdateUserCommand value, ConstraintValidatorContext context) { String inputEmail = value.getParam().getEmail(); diff --git a/src/main/java/io/spring/core/article/Article.java b/src/main/java/io/spring/core/article/Article.java index f23c2c6d5..fbd4fe345 100644 --- a/src/main/java/io/spring/core/article/Article.java +++ b/src/main/java/io/spring/core/article/Article.java @@ -11,25 +11,82 @@ import lombok.NoArgsConstructor; import org.joda.time.DateTime; +/** + * Represents an article in the RealWorld application. + * + *

This is a core domain entity that encapsulates all the information about an article, + * including its content, metadata, and associated tags. Articles are identified by a unique ID + * and can be accessed via a URL-friendly slug derived from the title. + * + *

The class uses Lombok annotations for boilerplate code generation: + *

    + *
  • {@code @Getter} - Generates getter methods for all fields
  • + *
  • {@code @NoArgsConstructor} - Generates a no-argument constructor for MyBatis
  • + *
  • {@code @EqualsAndHashCode} - Generates equals and hashCode based on the id field
  • + *
+ * + * @see Tag + * @see ArticleRepository + */ @Getter @NoArgsConstructor @EqualsAndHashCode(of = {"id"}) public class Article { + /** The unique identifier of the user who authored this article. */ private String userId; + + /** The unique identifier for this article. */ private String id; + + /** The URL-friendly slug derived from the article title. */ private String slug; + + /** The title of the article. */ private String title; + + /** A brief description or summary of the article. */ private String description; + + /** The main content/body of the article in markdown format. */ private String body; + + /** The list of tags associated with this article. */ private List tags; + + /** The timestamp when this article was created. */ private DateTime createdAt; + + /** The timestamp when this article was last updated. */ private DateTime updatedAt; + /** + * Creates a new article with the current timestamp. + * + * @param title the title of the article + * @param description a brief description of the article + * @param body the main content of the article + * @param tagList a list of tag names to associate with the article + * @param userId the unique identifier of the author + */ public Article( String title, String description, String body, List tagList, String userId) { this(title, description, body, tagList, userId, new DateTime()); } + /** + * Creates a new article with a specified creation timestamp. + * + *

This constructor generates a unique ID using UUID, creates a URL-friendly slug + * from the title, and converts the tag name list into Tag objects. Duplicate tags + * are automatically removed using a HashSet. + * + * @param title the title of the article + * @param description a brief description of the article + * @param body the main content of the article + * @param tagList a list of tag names to associate with the article + * @param userId the unique identifier of the author + * @param createdAt the timestamp for when the article was created + */ public Article( String title, String description, @@ -48,6 +105,17 @@ public Article( this.updatedAt = createdAt; } + /** + * Updates the article with new values. + * + *

Only non-empty values will be applied. If a field is updated, the {@code updatedAt} + * timestamp is automatically set to the current time. When the title is updated, + * the slug is also regenerated. + * + * @param title the new title, or null/empty to keep the current title + * @param description the new description, or null/empty to keep the current description + * @param body the new body content, or null/empty to keep the current body + */ public void update(String title, String description, String body) { if (!Util.isEmpty(title)) { this.title = title; @@ -64,6 +132,19 @@ public void update(String title, String description, String body) { } } + /** + * Converts a title string into a URL-friendly slug. + * + *

The conversion process: + *

    + *
  1. Converts the title to lowercase
  2. + *
  3. Replaces special characters, CJK punctuation, quotes, whitespace, + * question marks, commas, and periods with hyphens
  4. + *
+ * + * @param title the title to convert + * @return a URL-friendly slug representation of the title + */ public static String toSlug(String title) { return title.toLowerCase().replaceAll("[\\&|[\\uFE30-\\uFFA0]|\\’|\\”|\\s\\?\\,\\.]+", "-"); } diff --git a/src/main/java/io/spring/core/article/ArticleRepository.java b/src/main/java/io/spring/core/article/ArticleRepository.java index db9edf4cc..ef61797b5 100644 --- a/src/main/java/io/spring/core/article/ArticleRepository.java +++ b/src/main/java/io/spring/core/article/ArticleRepository.java @@ -2,13 +2,52 @@ import java.util.Optional; +/** + * Repository interface for managing {@link Article} entities. + * + *

This interface defines the contract for article persistence operations following + * the Repository pattern from Domain-Driven Design (DDD). It provides methods for + * creating, reading, updating, and deleting articles. + * + *

Implementations of this interface handle the actual persistence mechanism, + * such as MyBatis or JPA, while the domain layer remains agnostic of the + * underlying storage technology. + * + * @see Article + * @see io.spring.infrastructure.repository.MyBatisArticleRepository + */ public interface ArticleRepository { + /** + * Saves an article to the repository. + * + *

If the article already exists (based on its ID), it will be updated. + * Otherwise, a new article will be created. + * + * @param article the article to save + */ void save(Article article); + /** + * Finds an article by its unique identifier. + * + * @param id the unique identifier of the article + * @return an Optional containing the article if found, or empty if not found + */ Optional

findById(String id); + /** + * Finds an article by its URL-friendly slug. + * + * @param slug the slug of the article + * @return an Optional containing the article if found, or empty if not found + */ Optional
findBySlug(String slug); + /** + * Removes an article from the repository. + * + * @param article the article to remove + */ void remove(Article article); } diff --git a/src/main/java/io/spring/core/article/Tag.java b/src/main/java/io/spring/core/article/Tag.java index 27c736a58..54304557b 100644 --- a/src/main/java/io/spring/core/article/Tag.java +++ b/src/main/java/io/spring/core/article/Tag.java @@ -5,13 +5,40 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +/** + * Represents a tag that can be associated with articles. + * + *

Tags are used to categorize and organize articles, making it easier for users + * to discover content related to specific topics. Each tag has a unique identifier + * and a name. Tags are considered equal if they have the same name, regardless of + * their ID. + * + *

The class uses Lombok annotations for boilerplate code generation: + *

    + *
  • {@code @Data} - Generates getters, setters, toString, equals, and hashCode
  • + *
  • {@code @NoArgsConstructor} - Generates a no-argument constructor for MyBatis
  • + *
  • {@code @EqualsAndHashCode(of = "name")} - Uses only the name field for equality
  • + *
+ * + * @see Article + */ @NoArgsConstructor @Data @EqualsAndHashCode(of = "name") public class Tag { + /** The unique identifier for this tag. */ private String id; + + /** The display name of the tag. */ private String name; + /** + * Creates a new tag with the specified name. + * + *

A unique identifier is automatically generated using UUID. + * + * @param name the display name for the tag + */ public Tag(String name) { this.id = UUID.randomUUID().toString(); this.name = name; diff --git a/src/main/java/io/spring/core/comment/Comment.java b/src/main/java/io/spring/core/comment/Comment.java index 5b9fbe7d8..7a33edd94 100644 --- a/src/main/java/io/spring/core/comment/Comment.java +++ b/src/main/java/io/spring/core/comment/Comment.java @@ -6,16 +6,53 @@ import lombok.NoArgsConstructor; import org.joda.time.DateTime; +/** + * Represents a comment on an article in the RealWorld application. + * + *

This is a core domain entity that encapsulates all the information about a comment, + * including its content, the author, and the article it belongs to. Comments are + * identified by a unique ID and are associated with both a user (author) and an article. + * + *

The class uses Lombok annotations for boilerplate code generation: + *

    + *
  • {@code @Getter} - Generates getter methods for all fields
  • + *
  • {@code @NoArgsConstructor} - Generates a no-argument constructor for MyBatis
  • + *
  • {@code @EqualsAndHashCode} - Generates equals and hashCode based on the id field
  • + *
+ * + * @see CommentRepository + * @see io.spring.core.article.Article + * @see io.spring.core.user.User + */ @Getter @NoArgsConstructor @EqualsAndHashCode(of = "id") public class Comment { + /** The unique identifier for this comment. */ private String id; + + /** The content/body of the comment. */ private String body; + + /** The unique identifier of the user who authored this comment. */ private String userId; + + /** The unique identifier of the article this comment belongs to. */ private String articleId; + + /** The timestamp when this comment was created. */ private DateTime createdAt; + /** + * Creates a new comment with the specified content. + * + *

A unique identifier is automatically generated using UUID, and the + * creation timestamp is set to the current time. + * + * @param body the content of the comment + * @param userId the unique identifier of the comment author + * @param articleId the unique identifier of the article being commented on + */ public Comment(String body, String userId, String articleId) { this.id = UUID.randomUUID().toString(); this.body = body; diff --git a/src/main/java/io/spring/core/comment/CommentRepository.java b/src/main/java/io/spring/core/comment/CommentRepository.java index 154cafa5d..70c27d011 100644 --- a/src/main/java/io/spring/core/comment/CommentRepository.java +++ b/src/main/java/io/spring/core/comment/CommentRepository.java @@ -2,10 +2,41 @@ import java.util.Optional; +/** + * Repository interface for managing {@link Comment} entities. + * + *

This interface defines the contract for comment persistence operations following + * the Repository pattern from Domain-Driven Design (DDD). It provides methods for + * creating, reading, and deleting comments. + * + *

Implementations of this interface handle the actual persistence mechanism, + * such as MyBatis or JPA, while the domain layer remains agnostic of the + * underlying storage technology. + * + * @see Comment + * @see io.spring.infrastructure.repository.MyBatisCommentRepository + */ public interface CommentRepository { + /** + * Saves a comment to the repository. + * + * @param comment the comment to save + */ void save(Comment comment); + /** + * Finds a comment by its ID within a specific article. + * + * @param articleId the unique identifier of the article + * @param id the unique identifier of the comment + * @return an Optional containing the comment if found, or empty if not found + */ Optional findById(String articleId, String id); + /** + * Removes a comment from the repository. + * + * @param comment the comment to remove + */ void remove(Comment comment); } diff --git a/src/main/java/io/spring/core/favorite/ArticleFavorite.java b/src/main/java/io/spring/core/favorite/ArticleFavorite.java index 9dc115ee3..937ad9a94 100644 --- a/src/main/java/io/spring/core/favorite/ArticleFavorite.java +++ b/src/main/java/io/spring/core/favorite/ArticleFavorite.java @@ -4,13 +4,40 @@ import lombok.Getter; import lombok.NoArgsConstructor; +/** + * Represents a favorite relationship between a user and an article. + * + *

This is a domain entity that models the action of a user favoriting (liking) + * an article. The relationship is bidirectional: it links a user to an article + * they have favorited. + * + *

The class uses Lombok annotations for boilerplate code generation: + *

    + *
  • {@code @Getter} - Generates getter methods for all fields
  • + *
  • {@code @NoArgsConstructor} - Generates a no-argument constructor for MyBatis
  • + *
  • {@code @EqualsAndHashCode} - Generates equals and hashCode based on all fields
  • + *
+ * + * @see ArticleFavoriteRepository + * @see io.spring.core.article.Article + * @see io.spring.core.user.User + */ @NoArgsConstructor @Getter @EqualsAndHashCode public class ArticleFavorite { + /** The unique identifier of the favorited article. */ private String articleId; + + /** The unique identifier of the user who favorited the article. */ private String userId; + /** + * Creates a new article favorite relationship. + * + * @param articleId the unique identifier of the article being favorited + * @param userId the unique identifier of the user favoriting the article + */ public ArticleFavorite(String articleId, String userId) { this.articleId = articleId; this.userId = userId; diff --git a/src/main/java/io/spring/core/favorite/ArticleFavoriteRepository.java b/src/main/java/io/spring/core/favorite/ArticleFavoriteRepository.java index 6463c3f90..0a8b1cbe3 100644 --- a/src/main/java/io/spring/core/favorite/ArticleFavoriteRepository.java +++ b/src/main/java/io/spring/core/favorite/ArticleFavoriteRepository.java @@ -2,10 +2,42 @@ import java.util.Optional; +/** + * Repository interface for managing {@link ArticleFavorite} entities. + * + *

This interface defines the contract for article favorite persistence operations + * following the Repository pattern from Domain-Driven Design (DDD). It provides + * methods for creating, reading, and deleting favorite relationships between + * users and articles. + * + *

Implementations of this interface handle the actual persistence mechanism, + * such as MyBatis or JPA, while the domain layer remains agnostic of the + * underlying storage technology. + * + * @see ArticleFavorite + * @see io.spring.infrastructure.repository.MyBatisArticleFavoriteRepository + */ public interface ArticleFavoriteRepository { + /** + * Saves an article favorite relationship to the repository. + * + * @param articleFavorite the favorite relationship to save + */ void save(ArticleFavorite articleFavorite); + /** + * Finds a favorite relationship between a user and an article. + * + * @param articleId the unique identifier of the article + * @param userId the unique identifier of the user + * @return an Optional containing the favorite if found, or empty if not found + */ Optional find(String articleId, String userId); + /** + * Removes an article favorite relationship from the repository. + * + * @param favorite the favorite relationship to remove + */ void remove(ArticleFavorite favorite); } diff --git a/src/main/java/io/spring/core/service/AuthorizationService.java b/src/main/java/io/spring/core/service/AuthorizationService.java index b6a539d7b..f23568c53 100644 --- a/src/main/java/io/spring/core/service/AuthorizationService.java +++ b/src/main/java/io/spring/core/service/AuthorizationService.java @@ -4,11 +4,52 @@ import io.spring.core.comment.Comment; import io.spring.core.user.User; +/** + * Service class for authorization checks in the RealWorld application. + * + *

This class provides static utility methods to determine whether a user + * has permission to perform certain actions on articles and comments. + * It implements the authorization rules for the application's domain logic. + * + *

Authorization rules: + *

    + *
  • Only the author of an article can modify or delete it
  • + *
  • Both the article author and comment author can delete a comment
  • + *
+ * + * @see Article + * @see Comment + * @see User + */ public class AuthorizationService { + /** + * Checks if a user has permission to modify or delete an article. + * + *

Only the author of the article (the user whose ID matches the article's + * userId) is authorized to write (modify or delete) the article. + * + * @param user the user attempting to modify the article + * @param article the article to be modified + * @return {@code true} if the user is the author of the article, {@code false} otherwise + */ public static boolean canWriteArticle(User user, Article article) { return user.getId().equals(article.getUserId()); } + /** + * Checks if a user has permission to delete a comment. + * + *

A user can delete a comment if they are either: + *

    + *
  • The author of the article containing the comment
  • + *
  • The author of the comment itself
  • + *
+ * + * @param user the user attempting to delete the comment + * @param article the article containing the comment + * @param comment the comment to be deleted + * @return {@code true} if the user is authorized to delete the comment, {@code false} otherwise + */ public static boolean canWriteComment(User user, Article article, Comment comment) { return user.getId().equals(article.getUserId()) || user.getId().equals(comment.getUserId()); } diff --git a/src/main/java/io/spring/core/service/JwtService.java b/src/main/java/io/spring/core/service/JwtService.java index d1430768b..86d84fd76 100644 --- a/src/main/java/io/spring/core/service/JwtService.java +++ b/src/main/java/io/spring/core/service/JwtService.java @@ -4,9 +4,42 @@ import java.util.Optional; import org.springframework.stereotype.Service; +/** + * Service interface for JSON Web Token (JWT) operations. + * + *

This interface defines the contract for JWT-based authentication operations, + * including token generation and validation. JWTs are used to authenticate users + * in the RealWorld application without maintaining server-side session state. + * + *

Implementations of this interface handle the actual JWT encoding/decoding + * using libraries like JJWT, while the domain layer remains agnostic of the + * specific implementation details. + * + * @see User + * @see io.spring.infrastructure.service.DefaultJwtService + */ @Service public interface JwtService { + /** + * Generates a JWT token for the specified user. + * + *

The generated token typically contains the user's ID as the subject claim + * and has an expiration time configured in the application properties. + * + * @param user the user to generate a token for + * @return a signed JWT token string + */ String toToken(User user); + /** + * Extracts the subject (user ID) from a JWT token. + * + *

This method validates the token signature and expiration before + * extracting the subject claim. + * + * @param token the JWT token to parse + * @return an Optional containing the user ID if the token is valid, + * or empty if the token is invalid or expired + */ Optional getSubFromToken(String token); } diff --git a/src/main/java/io/spring/core/user/FollowRelation.java b/src/main/java/io/spring/core/user/FollowRelation.java index 7d7b53870..4ef4e65e5 100644 --- a/src/main/java/io/spring/core/user/FollowRelation.java +++ b/src/main/java/io/spring/core/user/FollowRelation.java @@ -3,12 +3,37 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Represents a follow relationship between two users. + * + *

This is a domain entity that models the social relationship where one user + * follows another user. The relationship is directional: the user identified by + * {@code userId} follows the user identified by {@code targetId}. + * + *

The class uses Lombok annotations for boilerplate code generation: + *

    + *
  • {@code @Data} - Generates getters, setters, toString, equals, and hashCode
  • + *
  • {@code @NoArgsConstructor} - Generates a no-argument constructor for MyBatis
  • + *
+ * + * @see User + * @see UserRepository + */ @NoArgsConstructor @Data public class FollowRelation { + /** The unique identifier of the user who is following. */ private String userId; + + /** The unique identifier of the user being followed. */ private String targetId; + /** + * Creates a new follow relationship. + * + * @param userId the ID of the user who is following + * @param targetId the ID of the user being followed + */ public FollowRelation(String userId, String targetId) { this.userId = userId; diff --git a/src/main/java/io/spring/core/user/User.java b/src/main/java/io/spring/core/user/User.java index 3044d5034..59ea9e835 100644 --- a/src/main/java/io/spring/core/user/User.java +++ b/src/main/java/io/spring/core/user/User.java @@ -6,17 +6,56 @@ import lombok.Getter; import lombok.NoArgsConstructor; +/** + * Represents a user in the RealWorld application. + * + *

This is a core domain entity that encapsulates all the information about a user, + * including authentication credentials and profile information. Users can create articles, + * post comments, follow other users, and favorite articles. + * + *

The class uses Lombok annotations for boilerplate code generation: + *

    + *
  • {@code @Getter} - Generates getter methods for all fields
  • + *
  • {@code @NoArgsConstructor} - Generates a no-argument constructor for MyBatis
  • + *
  • {@code @EqualsAndHashCode} - Generates equals and hashCode based on the id field
  • + *
+ * + * @see UserRepository + * @see FollowRelation + */ @Getter @NoArgsConstructor @EqualsAndHashCode(of = {"id"}) public class User { + /** The unique identifier for this user. */ private String id; + + /** The user's email address, used for authentication. */ private String email; + + /** The user's unique username, displayed publicly. */ private String username; + + /** The user's hashed password for authentication. */ private String password; + + /** The user's biography or description. */ private String bio; + + /** The URL to the user's profile image. */ private String image; + /** + * Creates a new user with the specified profile information. + * + *

A unique identifier is automatically generated using UUID. + * + * @param email the user's email address + * @param username the user's unique username + * @param password the user's hashed password + * @param bio the user's biography (can be null) + * @param image the URL to the user's profile image (can be null) + */ public User(String email, String username, String password, String bio, String image) { this.id = UUID.randomUUID().toString(); this.email = email; @@ -26,6 +65,18 @@ public User(String email, String username, String password, String bio, String i this.image = image; } + /** + * Updates the user's profile with new values. + * + *

Only non-empty values will be applied. Fields with null or empty values + * will retain their current values. + * + * @param email the new email address, or null/empty to keep the current email + * @param username the new username, or null/empty to keep the current username + * @param password the new hashed password, or null/empty to keep the current password + * @param bio the new biography, or null/empty to keep the current bio + * @param image the new profile image URL, or null/empty to keep the current image + */ public void update(String email, String username, String password, String bio, String image) { if (!Util.isEmpty(email)) { this.email = email; diff --git a/src/main/java/io/spring/core/user/UserRepository.java b/src/main/java/io/spring/core/user/UserRepository.java index f52c7725d..c89f39346 100644 --- a/src/main/java/io/spring/core/user/UserRepository.java +++ b/src/main/java/io/spring/core/user/UserRepository.java @@ -3,19 +3,80 @@ import java.util.Optional; import org.springframework.stereotype.Repository; +/** + * Repository interface for managing {@link User} entities and their relationships. + * + *

This interface defines the contract for user persistence operations following + * the Repository pattern from Domain-Driven Design (DDD). It provides methods for + * creating, reading, and managing users, as well as managing follow relationships + * between users. + * + *

Implementations of this interface handle the actual persistence mechanism, + * such as MyBatis or JPA, while the domain layer remains agnostic of the + * underlying storage technology. + * + * @see User + * @see FollowRelation + * @see io.spring.infrastructure.repository.MyBatisUserRepository + */ @Repository public interface UserRepository { + /** + * Saves a user to the repository. + * + *

If the user already exists (based on its ID), it will be updated. + * Otherwise, a new user will be created. + * + * @param user the user to save + */ void save(User user); + /** + * Finds a user by their unique identifier. + * + * @param id the unique identifier of the user + * @return an Optional containing the user if found, or empty if not found + */ Optional findById(String id); + /** + * Finds a user by their username. + * + * @param username the username to search for + * @return an Optional containing the user if found, or empty if not found + */ Optional findByUsername(String username); + /** + * Finds a user by their email address. + * + * @param email the email address to search for + * @return an Optional containing the user if found, or empty if not found + */ Optional findByEmail(String email); + /** + * Saves a follow relationship between two users. + * + *

This creates a relationship where one user follows another user. + * + * @param followRelation the follow relationship to save + */ void saveRelation(FollowRelation followRelation); + /** + * Finds a follow relationship between two users. + * + * @param userId the ID of the user who is following + * @param targetId the ID of the user being followed + * @return an Optional containing the relationship if found, or empty if not found + */ Optional findRelation(String userId, String targetId); + /** + * Removes a follow relationship between two users. + * + * @param followRelation the follow relationship to remove + */ void removeRelation(FollowRelation followRelation); } diff --git a/src/main/java/io/spring/graphql/ArticleDatafetcher.java b/src/main/java/io/spring/graphql/ArticleDatafetcher.java index 37c82939a..5d47c4698 100644 --- a/src/main/java/io/spring/graphql/ArticleDatafetcher.java +++ b/src/main/java/io/spring/graphql/ArticleDatafetcher.java @@ -32,11 +32,31 @@ import lombok.AllArgsConstructor; import org.joda.time.format.ISODateTimeFormat; +/** + * GraphQL data fetcher for article queries. + * + *

This component handles GraphQL queries related to articles, including: + *

    + *
  • User feed (articles from followed users)
  • + *
  • Article listing with filters
  • + *
  • User's authored articles
  • + *
  • User's favorited articles
  • + *
  • Single article by slug
  • + *
+ * + *

Uses cursor-based pagination for efficient data retrieval. + * + * @see ArticleQueryService + * @see ArticleMutation + */ @DgsComponent @AllArgsConstructor public class ArticleDatafetcher { + /** Service for querying article data. */ private ArticleQueryService articleQueryService; + + /** Repository for user operations. */ private UserRepository userRepository; @DgsQuery(field = QUERY.Feed) diff --git a/src/main/java/io/spring/graphql/ArticleMutation.java b/src/main/java/io/spring/graphql/ArticleMutation.java index 6b7b6eb2c..4295e93a4 100644 --- a/src/main/java/io/spring/graphql/ArticleMutation.java +++ b/src/main/java/io/spring/graphql/ArticleMutation.java @@ -24,12 +24,32 @@ import java.util.Collections; import lombok.AllArgsConstructor; +/** + * GraphQL mutation handler for article operations. + * + *

This component handles GraphQL mutations related to articles: + *

    + *
  • createArticle - Create a new article
  • + *
  • updateArticle - Update an existing article
  • + *
  • deleteArticle - Delete an article
  • + *
  • favoriteArticle - Add article to favorites
  • + *
  • unfavoriteArticle - Remove article from favorites
  • + *
+ * + * @see ArticleDatafetcher + * @see ArticleCommandService + */ @DgsComponent @AllArgsConstructor public class ArticleMutation { + /** Service for article write operations. */ private ArticleCommandService articleCommandService; + + /** Repository for article favorite operations. */ private ArticleFavoriteRepository articleFavoriteRepository; + + /** Repository for article persistence. */ private ArticleRepository articleRepository; @DgsMutation(field = MUTATION.CreateArticle) diff --git a/src/main/java/io/spring/graphql/CommentDatafetcher.java b/src/main/java/io/spring/graphql/CommentDatafetcher.java index 334a04c36..75f454d67 100644 --- a/src/main/java/io/spring/graphql/CommentDatafetcher.java +++ b/src/main/java/io/spring/graphql/CommentDatafetcher.java @@ -27,9 +27,22 @@ import lombok.AllArgsConstructor; import org.joda.time.format.ISODateTimeFormat; +/** + * GraphQL data fetcher for comment queries. + * + *

This component handles GraphQL queries related to comments: + *

    + *
  • Fetching comments for an article with cursor-based pagination
  • + *
  • Resolving comment payloads from mutations
  • + *
+ * + * @see CommentQueryService + * @see CommentMutation + */ @DgsComponent @AllArgsConstructor public class CommentDatafetcher { + /** Service for querying comment data. */ private CommentQueryService commentQueryService; @DgsData(parentType = COMMENTPAYLOAD.TYPE_NAME, field = COMMENTPAYLOAD.Comment) diff --git a/src/main/java/io/spring/graphql/CommentMutation.java b/src/main/java/io/spring/graphql/CommentMutation.java index 9a493f5f3..e0a9aeb30 100644 --- a/src/main/java/io/spring/graphql/CommentMutation.java +++ b/src/main/java/io/spring/graphql/CommentMutation.java @@ -20,12 +20,29 @@ import io.spring.graphql.types.DeletionStatus; import lombok.AllArgsConstructor; +/** + * GraphQL mutation handler for comment operations. + * + *

This component handles GraphQL mutations related to comments: + *

    + *
  • addComment - Add a comment to an article
  • + *
  • deleteComment - Delete a comment from an article
  • + *
+ * + * @see CommentDatafetcher + * @see CommentRepository + */ @DgsComponent @AllArgsConstructor public class CommentMutation { + /** Repository for article persistence. */ private ArticleRepository articleRepository; + + /** Repository for comment persistence. */ private CommentRepository commentRepository; + + /** Service for querying comment data. */ private CommentQueryService commentQueryService; @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.AddComment) diff --git a/src/main/java/io/spring/graphql/MeDatafetcher.java b/src/main/java/io/spring/graphql/MeDatafetcher.java index 939859677..b8dc6616d 100644 --- a/src/main/java/io/spring/graphql/MeDatafetcher.java +++ b/src/main/java/io/spring/graphql/MeDatafetcher.java @@ -18,10 +18,25 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.RequestHeader; +/** + * GraphQL data fetcher for current user (me) queries. + * + *

This component handles GraphQL queries related to the authenticated user: + *

    + *
  • me - Get the currently authenticated user's data
  • + *
  • Resolving user payloads from mutations
  • + *
+ * + * @see UserQueryService + * @see UserMutation + */ @DgsComponent @AllArgsConstructor public class MeDatafetcher { + /** Service for querying user data. */ private UserQueryService userQueryService; + + /** Service for JWT token generation. */ private JwtService jwtService; @DgsData(parentType = DgsConstants.QUERY_TYPE, field = QUERY.Me) diff --git a/src/main/java/io/spring/graphql/ProfileDatafetcher.java b/src/main/java/io/spring/graphql/ProfileDatafetcher.java index 989b77bba..2660dd02d 100644 --- a/src/main/java/io/spring/graphql/ProfileDatafetcher.java +++ b/src/main/java/io/spring/graphql/ProfileDatafetcher.java @@ -21,10 +21,24 @@ import java.util.Map; import lombok.AllArgsConstructor; +/** + * GraphQL data fetcher for user profile queries. + * + *

This component handles GraphQL queries related to user profiles: + *

    + *
  • profile - Get a user's profile by username
  • + *
  • Resolving author profiles for articles
  • + *
  • Resolving author profiles for comments
  • + *
+ * + * @see ProfileQueryService + * @see RelationMutation + */ @DgsComponent @AllArgsConstructor public class ProfileDatafetcher { + /** Service for querying profile data. */ private ProfileQueryService profileQueryService; @DgsData(parentType = USER.TYPE_NAME, field = USER.Profile) diff --git a/src/main/java/io/spring/graphql/RelationMutation.java b/src/main/java/io/spring/graphql/RelationMutation.java index 317b4fcc2..52b5f1d72 100644 --- a/src/main/java/io/spring/graphql/RelationMutation.java +++ b/src/main/java/io/spring/graphql/RelationMutation.java @@ -15,11 +15,26 @@ import io.spring.graphql.types.ProfilePayload; import lombok.AllArgsConstructor; +/** + * GraphQL mutation handler for user follow/unfollow operations. + * + *

This component handles GraphQL mutations related to user relationships: + *

    + *
  • followUser - Follow another user
  • + *
  • unfollowUser - Unfollow a user
  • + *
+ * + * @see ProfileDatafetcher + * @see FollowRelation + */ @DgsComponent @AllArgsConstructor public class RelationMutation { + /** Repository for user persistence and follow relationships. */ private UserRepository userRepository; + + /** Service for querying profile data. */ private ProfileQueryService profileQueryService; @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.FollowUser) diff --git a/src/main/java/io/spring/graphql/SecurityUtil.java b/src/main/java/io/spring/graphql/SecurityUtil.java index 24b723b23..2be06e158 100644 --- a/src/main/java/io/spring/graphql/SecurityUtil.java +++ b/src/main/java/io/spring/graphql/SecurityUtil.java @@ -6,7 +6,18 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +/** + * Utility class for security-related operations in GraphQL resolvers. + * + *

Provides methods to access the currently authenticated user from + * the Spring Security context. + */ public class SecurityUtil { + /** + * Gets the currently authenticated user from the security context. + * + * @return an Optional containing the current user, or empty if not authenticated + */ public static Optional getCurrentUser() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication instanceof AnonymousAuthenticationToken diff --git a/src/main/java/io/spring/graphql/TagDatafetcher.java b/src/main/java/io/spring/graphql/TagDatafetcher.java index 6b70bf5f8..1db0c785b 100644 --- a/src/main/java/io/spring/graphql/TagDatafetcher.java +++ b/src/main/java/io/spring/graphql/TagDatafetcher.java @@ -7,11 +7,27 @@ import java.util.List; import lombok.AllArgsConstructor; +/** + * GraphQL data fetcher for tag queries. + * + *

This component handles GraphQL queries related to tags: + *

    + *
  • tags - Get all available tags
  • + *
+ * + * @see TagsQueryService + */ @DgsComponent @AllArgsConstructor public class TagDatafetcher { + /** Service for querying tag data. */ private TagsQueryService tagsQueryService; + /** + * Retrieves all available tags. + * + * @return a list of all tag names + */ @DgsData(parentType = DgsConstants.QUERY_TYPE, field = QUERY.Tags) public List getTags() { return tagsQueryService.allTags(); diff --git a/src/main/java/io/spring/graphql/UserMutation.java b/src/main/java/io/spring/graphql/UserMutation.java index 581a5b7b5..b5ca4c677 100644 --- a/src/main/java/io/spring/graphql/UserMutation.java +++ b/src/main/java/io/spring/graphql/UserMutation.java @@ -25,12 +25,30 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; +/** + * GraphQL mutation handler for user operations. + * + *

This component handles GraphQL mutations related to users: + *

    + *
  • createUser - Register a new user
  • + *
  • login - Authenticate a user
  • + *
  • updateUser - Update the current user's profile
  • + *
+ * + * @see MeDatafetcher + * @see UserService + */ @DgsComponent @AllArgsConstructor public class UserMutation { + /** Repository for user persistence. */ private UserRepository userRepository; + + /** Encoder for password verification. */ private PasswordEncoder encryptService; + + /** Service for user write operations. */ private UserService userService; @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.CreateUser) diff --git a/src/main/java/io/spring/graphql/exception/AuthenticationException.java b/src/main/java/io/spring/graphql/exception/AuthenticationException.java index 417029f72..87df32091 100644 --- a/src/main/java/io/spring/graphql/exception/AuthenticationException.java +++ b/src/main/java/io/spring/graphql/exception/AuthenticationException.java @@ -1,3 +1,9 @@ package io.spring.graphql.exception; +/** + * Exception thrown when a GraphQL operation requires authentication. + * + *

This exception is thrown when a user attempts to perform a GraphQL + * mutation or query that requires authentication but is not logged in. + */ public class AuthenticationException extends RuntimeException {} diff --git a/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java b/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java index bf4768b3b..bbe5edd82 100644 --- a/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java +++ b/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java @@ -21,9 +21,22 @@ import javax.validation.ConstraintViolationException; import org.springframework.stereotype.Component; +/** + * Custom exception handler for GraphQL data fetchers. + * + *

This handler converts various exceptions into appropriate GraphQL errors: + *

    + *
  • {@link InvalidAuthenticationException} - Returns UNAUTHENTICATED error
  • + *
  • {@link ConstraintViolationException} - Returns BAD_REQUEST with field errors
  • + *
  • Other exceptions - Delegates to default handler
  • + *
+ * + * @see DataFetcherExceptionHandler + */ @Component public class GraphQLCustomizeExceptionHandler implements DataFetcherExceptionHandler { + /** Default handler for unhandled exceptions. */ private final DefaultDataFetcherExceptionHandler defaultHandler = new DefaultDataFetcherExceptionHandler(); diff --git a/src/main/java/io/spring/infrastructure/mybatis/DateTimeHandler.java b/src/main/java/io/spring/infrastructure/mybatis/DateTimeHandler.java index 19323e565..f22e72e21 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/DateTimeHandler.java +++ b/src/main/java/io/spring/infrastructure/mybatis/DateTimeHandler.java @@ -12,9 +12,19 @@ import org.apache.ibatis.type.TypeHandler; import org.joda.time.DateTime; +/** + * MyBatis type handler for Joda-Time DateTime objects. + * + *

This handler converts between Joda-Time {@link DateTime} objects and + * SQL {@link Timestamp} values, ensuring all timestamps are stored and + * retrieved in UTC timezone. + * + * @see TypeHandler + */ @MappedTypes(DateTime.class) public class DateTimeHandler implements TypeHandler { + /** Calendar instance configured for UTC timezone. */ private static final Calendar UTC_CALENDAR = Calendar.getInstance(TimeZone.getTimeZone("UTC")); @Override diff --git a/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleFavoriteMapper.java b/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleFavoriteMapper.java index 2d4407218..db60ef120 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleFavoriteMapper.java +++ b/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleFavoriteMapper.java @@ -4,11 +4,36 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +/** + * MyBatis mapper interface for article favorite operations. + * + *

This mapper provides database operations for managing article favorites, + * including finding, inserting, and deleting favorite relationships. + * + * @see ArticleFavorite + */ @Mapper public interface ArticleFavoriteMapper { + /** + * Finds a favorite relationship between an article and a user. + * + * @param articleId the article ID + * @param userId the user ID + * @return the favorite relationship, or null if not found + */ ArticleFavorite find(@Param("articleId") String articleId, @Param("userId") String userId); + /** + * Inserts a new article favorite relationship. + * + * @param articleFavorite the favorite relationship to insert + */ void insert(@Param("articleFavorite") ArticleFavorite articleFavorite); + /** + * Deletes an article favorite relationship. + * + * @param favorite the favorite relationship to delete + */ void delete(@Param("favorite") ArticleFavorite favorite); } diff --git a/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleMapper.java b/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleMapper.java index 2facc3b14..d729236d6 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleMapper.java +++ b/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleMapper.java @@ -5,21 +5,74 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +/** + * MyBatis mapper interface for article operations. + * + *

This mapper provides database operations for articles and tags, + * including CRUD operations and tag relationship management. + * + * @see Article + * @see Tag + */ @Mapper public interface ArticleMapper { + /** + * Inserts a new article into the database. + * + * @param article the article to insert + */ void insert(@Param("article") Article article); + /** + * Finds an article by its ID. + * + * @param id the article ID + * @return the article, or null if not found + */ Article findById(@Param("id") String id); + /** + * Finds a tag by its name. + * + * @param tagName the tag name + * @return the tag, or null if not found + */ Tag findTag(@Param("tagName") String tagName); + /** + * Inserts a new tag into the database. + * + * @param tag the tag to insert + */ void insertTag(@Param("tag") Tag tag); + /** + * Creates a relationship between an article and a tag. + * + * @param articleId the article ID + * @param tagId the tag ID + */ void insertArticleTagRelation(@Param("articleId") String articleId, @Param("tagId") String tagId); + /** + * Finds an article by its slug. + * + * @param slug the article slug + * @return the article, or null if not found + */ Article findBySlug(@Param("slug") String slug); + /** + * Updates an existing article. + * + * @param article the article with updated values + */ void update(@Param("article") Article article); + /** + * Deletes an article by its ID. + * + * @param id the article ID + */ void delete(@Param("id") String id); } diff --git a/src/main/java/io/spring/infrastructure/mybatis/mapper/CommentMapper.java b/src/main/java/io/spring/infrastructure/mybatis/mapper/CommentMapper.java index 7137a25ad..f2c915f41 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/mapper/CommentMapper.java +++ b/src/main/java/io/spring/infrastructure/mybatis/mapper/CommentMapper.java @@ -4,11 +4,36 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +/** + * MyBatis mapper interface for comment operations. + * + *

This mapper provides database operations for comments, + * including inserting, finding, and deleting comments. + * + * @see Comment + */ @Mapper public interface CommentMapper { + /** + * Inserts a new comment into the database. + * + * @param comment the comment to insert + */ void insert(@Param("comment") Comment comment); + /** + * Finds a comment by article ID and comment ID. + * + * @param articleId the article ID + * @param id the comment ID + * @return the comment, or null if not found + */ Comment findById(@Param("articleId") String articleId, @Param("id") String id); + /** + * Deletes a comment by its ID. + * + * @param id the comment ID + */ void delete(@Param("id") String id); } diff --git a/src/main/java/io/spring/infrastructure/mybatis/mapper/UserMapper.java b/src/main/java/io/spring/infrastructure/mybatis/mapper/UserMapper.java index 54f36c76a..dcf1b3c66 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/mapper/UserMapper.java +++ b/src/main/java/io/spring/infrastructure/mybatis/mapper/UserMapper.java @@ -5,21 +5,75 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +/** + * MyBatis mapper interface for user operations. + * + *

This mapper provides database operations for users and follow relationships, + * including CRUD operations and relationship management. + * + * @see User + * @see FollowRelation + */ @Mapper public interface UserMapper { + /** + * Inserts a new user into the database. + * + * @param user the user to insert + */ void insert(@Param("user") User user); + /** + * Finds a user by username. + * + * @param username the username + * @return the user, or null if not found + */ User findByUsername(@Param("username") String username); + /** + * Finds a user by email address. + * + * @param email the email address + * @return the user, or null if not found + */ User findByEmail(@Param("email") String email); + /** + * Finds a user by ID. + * + * @param id the user ID + * @return the user, or null if not found + */ User findById(@Param("id") String id); + /** + * Updates an existing user. + * + * @param user the user with updated values + */ void update(@Param("user") User user); + /** + * Finds a follow relationship between two users. + * + * @param userId the follower's user ID + * @param targetId the followed user's ID + * @return the follow relationship, or null if not found + */ FollowRelation findRelation(@Param("userId") String userId, @Param("targetId") String targetId); + /** + * Saves a new follow relationship. + * + * @param followRelation the follow relationship to save + */ void saveRelation(@Param("followRelation") FollowRelation followRelation); + /** + * Deletes a follow relationship. + * + * @param followRelation the follow relationship to delete + */ void deleteRelation(@Param("followRelation") FollowRelation followRelation); } diff --git a/src/main/java/io/spring/infrastructure/mybatis/readservice/ArticleFavoritesReadService.java b/src/main/java/io/spring/infrastructure/mybatis/readservice/ArticleFavoritesReadService.java index 21510308a..0d09b0d93 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/readservice/ArticleFavoritesReadService.java +++ b/src/main/java/io/spring/infrastructure/mybatis/readservice/ArticleFavoritesReadService.java @@ -7,13 +7,47 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +/** + * MyBatis read service for article favorites queries. + * + *

This service provides read-only queries for article favorite data, + * optimized for the CQRS read model. + * + * @see ArticleFavoriteCount + */ @Mapper public interface ArticleFavoritesReadService { + /** + * Checks if a user has favorited an article. + * + * @param userId the user ID + * @param articleId the article ID + * @return true if the user has favorited the article + */ boolean isUserFavorite(@Param("userId") String userId, @Param("articleId") String articleId); + /** + * Gets the favorite count for an article. + * + * @param articleId the article ID + * @return the number of users who have favorited the article + */ int articleFavoriteCount(@Param("articleId") String articleId); + /** + * Gets favorite counts for multiple articles. + * + * @param ids the list of article IDs + * @return a list of favorite counts for each article + */ List articlesFavoriteCount(@Param("ids") List ids); + /** + * Gets the set of article IDs that a user has favorited. + * + * @param ids the list of article IDs to check + * @param currentUser the current user + * @return a set of article IDs that the user has favorited + */ Set userFavorites(@Param("ids") List ids, @Param("currentUser") User currentUser); } diff --git a/src/main/java/io/spring/infrastructure/mybatis/readservice/ArticleReadService.java b/src/main/java/io/spring/infrastructure/mybatis/readservice/ArticleReadService.java index 3075a3df9..38663e9d7 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/readservice/ArticleReadService.java +++ b/src/main/java/io/spring/infrastructure/mybatis/readservice/ArticleReadService.java @@ -7,33 +7,105 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +/** + * MyBatis read service for article queries. + * + *

This service provides read-only queries for article data, + * supporting both offset-based and cursor-based pagination. + * + * @see ArticleData + */ @Mapper public interface ArticleReadService { + /** + * Finds an article by its ID. + * + * @param id the article ID + * @return the article data, or null if not found + */ ArticleData findById(@Param("id") String id); + /** + * Finds an article by its slug. + * + * @param slug the article slug + * @return the article data, or null if not found + */ ArticleData findBySlug(@Param("slug") String slug); + /** + * Queries article IDs with filters and offset-based pagination. + * + * @param tag filter by tag name (optional) + * @param author filter by author username (optional) + * @param favoritedBy filter by user who favorited (optional) + * @param page pagination parameters + * @return a list of article IDs matching the criteria + */ List queryArticles( @Param("tag") String tag, @Param("author") String author, @Param("favoritedBy") String favoritedBy, @Param("page") Page page); + /** + * Counts articles matching the given filters. + * + * @param tag filter by tag name (optional) + * @param author filter by author username (optional) + * @param favoritedBy filter by user who favorited (optional) + * @return the count of matching articles + */ int countArticle( @Param("tag") String tag, @Param("author") String author, @Param("favoritedBy") String favoritedBy); + /** + * Finds articles by their IDs. + * + * @param articleIds the list of article IDs + * @return a list of article data + */ List findArticles(@Param("articleIds") List articleIds); + /** + * Finds articles by authors with offset-based pagination. + * + * @param authors the list of author user IDs + * @param page pagination parameters + * @return a list of article data + */ List findArticlesOfAuthors( @Param("authors") List authors, @Param("page") Page page); + /** + * Finds articles by authors with cursor-based pagination. + * + * @param authors the list of author user IDs + * @param page cursor pagination parameters + * @return a list of article data + */ List findArticlesOfAuthorsWithCursor( @Param("authors") List authors, @Param("page") CursorPageParameter page); + /** + * Counts the total feed size for the given authors. + * + * @param authors the list of author user IDs + * @return the total count of articles + */ int countFeedSize(@Param("authors") List authors); + /** + * Queries article IDs with filters and cursor-based pagination. + * + * @param tag filter by tag name (optional) + * @param author filter by author username (optional) + * @param favoritedBy filter by user who favorited (optional) + * @param page cursor pagination parameters + * @return a list of article IDs matching the criteria + */ List findArticlesWithCursor( @Param("tag") String tag, @Param("author") String author, diff --git a/src/main/java/io/spring/infrastructure/mybatis/readservice/CommentReadService.java b/src/main/java/io/spring/infrastructure/mybatis/readservice/CommentReadService.java index 1f7f1c159..5cc101dda 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/readservice/CommentReadService.java +++ b/src/main/java/io/spring/infrastructure/mybatis/readservice/CommentReadService.java @@ -7,12 +7,39 @@ import org.apache.ibatis.annotations.Param; import org.joda.time.DateTime; +/** + * MyBatis read service for comment queries. + * + *

This service provides read-only queries for comment data, + * supporting both simple and cursor-based pagination. + * + * @see CommentData + */ @Mapper public interface CommentReadService { + /** + * Finds a comment by its ID. + * + * @param id the comment ID + * @return the comment data, or null if not found + */ CommentData findById(@Param("id") String id); + /** + * Finds all comments for an article. + * + * @param articleId the article ID + * @return a list of comment data + */ List findByArticleId(@Param("articleId") String articleId); + /** + * Finds comments for an article with cursor-based pagination. + * + * @param articleId the article ID + * @param page cursor pagination parameters + * @return a list of comment data + */ List findByArticleIdWithCursor( @Param("articleId") String articleId, @Param("page") CursorPageParameter page); } diff --git a/src/main/java/io/spring/infrastructure/mybatis/readservice/TagReadService.java b/src/main/java/io/spring/infrastructure/mybatis/readservice/TagReadService.java index 873768746..dc06352f6 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/readservice/TagReadService.java +++ b/src/main/java/io/spring/infrastructure/mybatis/readservice/TagReadService.java @@ -3,7 +3,17 @@ import java.util.List; import org.apache.ibatis.annotations.Mapper; +/** + * MyBatis read service for tag queries. + * + *

This service provides read-only queries for tag data. + */ @Mapper public interface TagReadService { + /** + * Retrieves all tag names. + * + * @return a list of all tag names + */ List all(); } diff --git a/src/main/java/io/spring/infrastructure/mybatis/readservice/UserReadService.java b/src/main/java/io/spring/infrastructure/mybatis/readservice/UserReadService.java index ae25a48de..0caec22b5 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/readservice/UserReadService.java +++ b/src/main/java/io/spring/infrastructure/mybatis/readservice/UserReadService.java @@ -4,10 +4,29 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +/** + * MyBatis read service for user queries. + * + *

This service provides read-only queries for user data. + * + * @see UserData + */ @Mapper public interface UserReadService { + /** + * Finds a user by username. + * + * @param username the username + * @return the user data, or null if not found + */ UserData findByUsername(@Param("username") String username); + /** + * Finds a user by ID. + * + * @param id the user ID + * @return the user data, or null if not found + */ UserData findById(@Param("id") String id); } diff --git a/src/main/java/io/spring/infrastructure/mybatis/readservice/UserRelationshipQueryService.java b/src/main/java/io/spring/infrastructure/mybatis/readservice/UserRelationshipQueryService.java index b85fa3826..0c3ca8d19 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/readservice/UserRelationshipQueryService.java +++ b/src/main/java/io/spring/infrastructure/mybatis/readservice/UserRelationshipQueryService.java @@ -5,12 +5,37 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +/** + * MyBatis read service for user relationship queries. + * + *

This service provides read-only queries for follow relationships. + */ @Mapper public interface UserRelationshipQueryService { + /** + * Checks if a user is following another user. + * + * @param userId the follower's user ID + * @param anotherUserId the followed user's ID + * @return true if the user is following the other user + */ boolean isUserFollowing( @Param("userId") String userId, @Param("anotherUserId") String anotherUserId); + /** + * Gets the set of authors that a user is following from a given list. + * + * @param userId the user ID + * @param ids the list of author IDs to check + * @return a set of author IDs that the user is following + */ Set followingAuthors(@Param("userId") String userId, @Param("ids") List ids); + /** + * Gets all users that a user is following. + * + * @param userId the user ID + * @return a list of user IDs that the user is following + */ List followedUsers(@Param("userId") String userId); } diff --git a/src/main/java/io/spring/infrastructure/repository/MyBatisArticleFavoriteRepository.java b/src/main/java/io/spring/infrastructure/repository/MyBatisArticleFavoriteRepository.java index 21ae54ac0..0dedd4191 100644 --- a/src/main/java/io/spring/infrastructure/repository/MyBatisArticleFavoriteRepository.java +++ b/src/main/java/io/spring/infrastructure/repository/MyBatisArticleFavoriteRepository.java @@ -7,8 +7,18 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; +/** + * MyBatis implementation of the ArticleFavoriteRepository. + * + *

This repository provides persistence operations for article favorites + * using MyBatis as the ORM framework. + * + * @see ArticleFavoriteRepository + * @see ArticleFavoriteMapper + */ @Repository public class MyBatisArticleFavoriteRepository implements ArticleFavoriteRepository { + /** MyBatis mapper for article favorite operations. */ private ArticleFavoriteMapper mapper; @Autowired diff --git a/src/main/java/io/spring/infrastructure/repository/MyBatisArticleRepository.java b/src/main/java/io/spring/infrastructure/repository/MyBatisArticleRepository.java index d252ee662..c4e76f91b 100644 --- a/src/main/java/io/spring/infrastructure/repository/MyBatisArticleRepository.java +++ b/src/main/java/io/spring/infrastructure/repository/MyBatisArticleRepository.java @@ -8,8 +8,19 @@ import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +/** + * MyBatis implementation of the ArticleRepository. + * + *

This repository provides persistence operations for articles and tags + * using MyBatis as the ORM framework. It handles article creation, updates, + * and tag relationship management within transactions. + * + * @see ArticleRepository + * @see ArticleMapper + */ @Repository public class MyBatisArticleRepository implements ArticleRepository { + /** MyBatis mapper for article operations. */ private ArticleMapper articleMapper; public MyBatisArticleRepository(ArticleMapper articleMapper) { diff --git a/src/main/java/io/spring/infrastructure/repository/MyBatisCommentRepository.java b/src/main/java/io/spring/infrastructure/repository/MyBatisCommentRepository.java index 8be451681..dd71be38b 100644 --- a/src/main/java/io/spring/infrastructure/repository/MyBatisCommentRepository.java +++ b/src/main/java/io/spring/infrastructure/repository/MyBatisCommentRepository.java @@ -7,8 +7,18 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +/** + * MyBatis implementation of the CommentRepository. + * + *

This repository provides persistence operations for comments + * using MyBatis as the ORM framework. + * + * @see CommentRepository + * @see CommentMapper + */ @Component public class MyBatisCommentRepository implements CommentRepository { + /** MyBatis mapper for comment operations. */ private CommentMapper commentMapper; @Autowired diff --git a/src/main/java/io/spring/infrastructure/repository/MyBatisUserRepository.java b/src/main/java/io/spring/infrastructure/repository/MyBatisUserRepository.java index 3c24dd5f0..9016f83fe 100644 --- a/src/main/java/io/spring/infrastructure/repository/MyBatisUserRepository.java +++ b/src/main/java/io/spring/infrastructure/repository/MyBatisUserRepository.java @@ -8,8 +8,18 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; +/** + * MyBatis implementation of the UserRepository. + * + *

This repository provides persistence operations for users and follow + * relationships using MyBatis as the ORM framework. + * + * @see UserRepository + * @see UserMapper + */ @Repository public class MyBatisUserRepository implements UserRepository { + /** MyBatis mapper for user operations. */ private final UserMapper userMapper; @Autowired diff --git a/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java b/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java index 515d66106..37d201de1 100644 --- a/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java +++ b/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java @@ -14,10 +14,24 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +/** + * Default implementation of the JwtService using JJWT library. + * + *

This service handles JWT token generation and validation using + * HS512 signature algorithm. Tokens contain the user ID as the subject + * and have a configurable expiration time. + * + * @see JwtService + */ @Component public class DefaultJwtService implements JwtService { + /** Secret key for signing JWT tokens. */ private final SecretKey signingKey; + + /** Algorithm used for signing tokens. */ private final SignatureAlgorithm signatureAlgorithm; + + /** Session duration in seconds. */ private int sessionTime; @Autowired diff --git a/src/test/java/io/spring/RealworldApplicationTests.java b/src/test/java/io/spring/RealworldApplicationTests.java index bf36bbdc1..f33772ff0 100644 --- a/src/test/java/io/spring/RealworldApplicationTests.java +++ b/src/test/java/io/spring/RealworldApplicationTests.java @@ -3,9 +3,17 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +/** + * Integration tests for the RealWorld application. + * + *

Verifies that the Spring application context loads correctly. + */ @SpringBootTest public class RealworldApplicationTests { + /** + * Verifies that the Spring application context loads without errors. + */ @Test public void contextLoads() {} } diff --git a/src/test/java/io/spring/TestHelper.java b/src/test/java/io/spring/TestHelper.java index dcd57071c..fe762fb9e 100644 --- a/src/test/java/io/spring/TestHelper.java +++ b/src/test/java/io/spring/TestHelper.java @@ -8,7 +8,20 @@ import java.util.Arrays; import org.joda.time.DateTime; +/** + * Helper class for creating test fixtures. + * + *

Provides utility methods for generating test data objects + * used across multiple test classes. + */ public class TestHelper { + /** + * Creates an ArticleData fixture with the given seed and user. + * + * @param seed a unique seed for generating test data + * @param user the user to associate as the article author + * @return an ArticleData instance with generated test values + */ public static ArticleData articleDataFixture(String seed, User user) { DateTime now = new DateTime(); return new ArticleData( @@ -25,6 +38,13 @@ public static ArticleData articleDataFixture(String seed, User user) { new ProfileData(user.getId(), user.getUsername(), user.getBio(), user.getImage(), false)); } + /** + * Creates an ArticleData from an existing Article and User. + * + * @param article the article to convert + * @param user the user to associate as the article author + * @return an ArticleData instance based on the article and user + */ public static ArticleData getArticleDataFromArticleAndUser(Article article, User user) { return new ArticleData( article.getId(), diff --git a/src/test/java/io/spring/api/ArticleApiTest.java b/src/test/java/io/spring/api/ArticleApiTest.java index df2ebe755..e3eb889eb 100644 --- a/src/test/java/io/spring/api/ArticleApiTest.java +++ b/src/test/java/io/spring/api/ArticleApiTest.java @@ -34,6 +34,12 @@ import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; +/** + * Unit tests for the ArticleApi REST controller. + * + *

Tests article CRUD operations including reading, updating, and deleting articles, + * as well as authorization checks for article modifications. + */ @WebMvcTest({ArticleApi.class}) @Import({WebSecurityConfig.class, JacksonCustomizations.class}) public class ArticleApiTest extends TestWithCurrentUser { diff --git a/src/test/java/io/spring/api/ArticleFavoriteApiTest.java b/src/test/java/io/spring/api/ArticleFavoriteApiTest.java index 7a609a255..43448385c 100644 --- a/src/test/java/io/spring/api/ArticleFavoriteApiTest.java +++ b/src/test/java/io/spring/api/ArticleFavoriteApiTest.java @@ -30,6 +30,11 @@ import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; +/** + * Unit tests for the ArticleFavoriteApi REST controller. + * + *

Tests article favorite and unfavorite operations. + */ @WebMvcTest(ArticleFavoriteApi.class) @Import({WebSecurityConfig.class, JacksonCustomizations.class}) public class ArticleFavoriteApiTest extends TestWithCurrentUser { diff --git a/src/test/java/io/spring/api/ArticlesApiTest.java b/src/test/java/io/spring/api/ArticlesApiTest.java index 18948417d..e5d8e2e3d 100644 --- a/src/test/java/io/spring/api/ArticlesApiTest.java +++ b/src/test/java/io/spring/api/ArticlesApiTest.java @@ -29,6 +29,12 @@ import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; +/** + * Unit tests for the ArticlesApi REST controller. + * + *

Tests article creation with validation, including duplicate title detection + * and required field validation. + */ @WebMvcTest({ArticlesApi.class}) @Import({WebSecurityConfig.class, JacksonCustomizations.class}) public class ArticlesApiTest extends TestWithCurrentUser { diff --git a/src/test/java/io/spring/api/CommentsApiTest.java b/src/test/java/io/spring/api/CommentsApiTest.java index 49700abe6..7d148bcbc 100644 --- a/src/test/java/io/spring/api/CommentsApiTest.java +++ b/src/test/java/io/spring/api/CommentsApiTest.java @@ -30,6 +30,12 @@ import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; +/** + * Unit tests for the CommentsApi REST controller. + * + *

Tests comment CRUD operations including creating, reading, and deleting comments, + * as well as authorization checks for comment deletion. + */ @WebMvcTest(CommentsApi.class) @Import({WebSecurityConfig.class, JacksonCustomizations.class}) public class CommentsApiTest extends TestWithCurrentUser { diff --git a/src/test/java/io/spring/api/CurrentUserApiTest.java b/src/test/java/io/spring/api/CurrentUserApiTest.java index 08e8ece2e..425bf260f 100644 --- a/src/test/java/io/spring/api/CurrentUserApiTest.java +++ b/src/test/java/io/spring/api/CurrentUserApiTest.java @@ -25,6 +25,12 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.test.web.servlet.MockMvc; +/** + * Unit tests for the CurrentUserApi REST controller. + * + *

Tests current user operations including getting and updating user profile, + * as well as authentication validation. + */ @WebMvcTest(CurrentUserApi.class) @Import({ WebSecurityConfig.class, diff --git a/src/test/java/io/spring/api/ListArticleApiTest.java b/src/test/java/io/spring/api/ListArticleApiTest.java index 032850bce..04fb92452 100644 --- a/src/test/java/io/spring/api/ListArticleApiTest.java +++ b/src/test/java/io/spring/api/ListArticleApiTest.java @@ -22,6 +22,11 @@ import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; +/** + * Unit tests for article listing and feed endpoints. + * + *

Tests article list retrieval and user feed functionality. + */ @WebMvcTest(ArticlesApi.class) @Import({WebSecurityConfig.class, JacksonCustomizations.class}) public class ListArticleApiTest extends TestWithCurrentUser { diff --git a/src/test/java/io/spring/api/ProfileApiTest.java b/src/test/java/io/spring/api/ProfileApiTest.java index f32091ecd..37cbb1153 100644 --- a/src/test/java/io/spring/api/ProfileApiTest.java +++ b/src/test/java/io/spring/api/ProfileApiTest.java @@ -22,6 +22,11 @@ import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; +/** + * Unit tests for the ProfileApi REST controller. + * + *

Tests profile viewing and follow/unfollow operations. + */ @WebMvcTest(ProfileApi.class) @Import({WebSecurityConfig.class, JacksonCustomizations.class}) public class ProfileApiTest extends TestWithCurrentUser { diff --git a/src/test/java/io/spring/api/TestWithCurrentUser.java b/src/test/java/io/spring/api/TestWithCurrentUser.java index 7d3b104b3..83120ab67 100644 --- a/src/test/java/io/spring/api/TestWithCurrentUser.java +++ b/src/test/java/io/spring/api/TestWithCurrentUser.java @@ -12,6 +12,16 @@ import org.junit.jupiter.api.BeforeEach; import org.springframework.boot.test.mock.mockito.MockBean; +/** + * Abstract base class for API tests that require an authenticated user. + * + *

Provides common setup for mocking user authentication, including: + *

    + *
  • User repository mocks
  • + *
  • JWT service mocks
  • + *
  • User read service mocks
  • + *
+ */ abstract class TestWithCurrentUser { @MockBean protected UserRepository userRepository; diff --git a/src/test/java/io/spring/api/UsersApiTest.java b/src/test/java/io/spring/api/UsersApiTest.java index 9074f2edc..2221d6b0b 100644 --- a/src/test/java/io/spring/api/UsersApiTest.java +++ b/src/test/java/io/spring/api/UsersApiTest.java @@ -30,6 +30,12 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.web.servlet.MockMvc; +/** + * Unit tests for the UsersApi REST controller. + * + *

Tests user registration and login operations, including validation + * for duplicate usernames, emails, and invalid credentials. + */ @WebMvcTest(UsersApi.class) @Import({ WebSecurityConfig.class, diff --git a/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java b/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java index 96229376c..8a577a55e 100644 --- a/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java +++ b/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java @@ -28,6 +28,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; +/** + * Integration tests for the ArticleQueryService. + * + *

Tests article query operations including fetching articles by ID, + * listing articles with various filters (tag, author, favorited), + * cursor-based pagination, and user feed functionality. + */ @Import({ ArticleQueryService.class, MyBatisUserRepository.class, diff --git a/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java b/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java index e712c90ab..3e231a389 100644 --- a/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java +++ b/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java @@ -22,6 +22,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; +/** + * Integration tests for the CommentQueryService. + * + *

Tests comment query operations including fetching comments by ID + * and retrieving all comments for an article. + */ @Import({ MyBatisCommentRepository.class, MyBatisUserRepository.class, diff --git a/src/test/java/io/spring/application/profile/ProfileQueryServiceTest.java b/src/test/java/io/spring/application/profile/ProfileQueryServiceTest.java index 34ce502d3..221d5a66b 100644 --- a/src/test/java/io/spring/application/profile/ProfileQueryServiceTest.java +++ b/src/test/java/io/spring/application/profile/ProfileQueryServiceTest.java @@ -12,6 +12,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; +/** + * Integration tests for the ProfileQueryService. + * + *

Tests profile query operations including fetching user profiles by username. + */ @Import({ProfileQueryService.class, MyBatisUserRepository.class}) public class ProfileQueryServiceTest extends DbTestBase { @Autowired private ProfileQueryService profileQueryService; diff --git a/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java b/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java index 620737972..b69832e3c 100644 --- a/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java +++ b/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java @@ -11,6 +11,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; +/** + * Integration tests for the TagsQueryService. + * + *

Tests tag query operations including retrieving all tags from articles. + */ @Import({TagsQueryService.class, MyBatisArticleRepository.class}) public class TagsQueryServiceTest extends DbTestBase { @Autowired private TagsQueryService tagsQueryService; diff --git a/src/test/java/io/spring/core/article/ArticleTest.java b/src/test/java/io/spring/core/article/ArticleTest.java index 3e5bdb329..7a247000a 100644 --- a/src/test/java/io/spring/core/article/ArticleTest.java +++ b/src/test/java/io/spring/core/article/ArticleTest.java @@ -6,6 +6,12 @@ import java.util.Arrays; import org.junit.jupiter.api.Test; +/** + * Unit tests for the Article domain entity. + * + *

Tests slug generation from article titles, including handling of + * special characters, numbers, case conversion, and non-ASCII characters. + */ public class ArticleTest { @Test diff --git a/src/test/java/io/spring/infrastructure/DbTestBase.java b/src/test/java/io/spring/infrastructure/DbTestBase.java index 80ed81cb6..5ca48e0b6 100644 --- a/src/test/java/io/spring/infrastructure/DbTestBase.java +++ b/src/test/java/io/spring/infrastructure/DbTestBase.java @@ -5,6 +5,12 @@ import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; import org.springframework.test.context.ActiveProfiles; +/** + * Abstract base class for database integration tests. + * + *

Configures MyBatis test environment with the test profile + * and uses the actual database instead of an embedded one. + */ @ActiveProfiles("test") @AutoConfigureTestDatabase(replace = Replace.NONE) @MybatisTest diff --git a/src/test/java/io/spring/infrastructure/article/ArticleRepositoryTransactionTest.java b/src/test/java/io/spring/infrastructure/article/ArticleRepositoryTransactionTest.java index 2ea0047d6..f6e4d7b2e 100644 --- a/src/test/java/io/spring/infrastructure/article/ArticleRepositoryTransactionTest.java +++ b/src/test/java/io/spring/infrastructure/article/ArticleRepositoryTransactionTest.java @@ -13,6 +13,12 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +/** + * Integration tests for article repository transaction behavior. + * + *

Tests that database transactions are properly rolled back when + * duplicate articles are saved, ensuring data consistency. + */ @ActiveProfiles("test") @SpringBootTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) diff --git a/src/test/java/io/spring/infrastructure/article/MyBatisArticleRepositoryTest.java b/src/test/java/io/spring/infrastructure/article/MyBatisArticleRepositoryTest.java index 0948af0cf..df70295e0 100644 --- a/src/test/java/io/spring/infrastructure/article/MyBatisArticleRepositoryTest.java +++ b/src/test/java/io/spring/infrastructure/article/MyBatisArticleRepositoryTest.java @@ -16,6 +16,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; +/** + * Integration tests for the MyBatisArticleRepository. + * + *

Tests article CRUD operations including creating, reading, updating, + * and deleting articles with their associated tags. + */ @Import({MyBatisArticleRepository.class, MyBatisUserRepository.class}) public class MyBatisArticleRepositoryTest extends DbTestBase { @Autowired private ArticleRepository articleRepository; diff --git a/src/test/java/io/spring/infrastructure/comment/MyBatisCommentRepositoryTest.java b/src/test/java/io/spring/infrastructure/comment/MyBatisCommentRepositoryTest.java index 8cc9a66eb..adac1bb67 100644 --- a/src/test/java/io/spring/infrastructure/comment/MyBatisCommentRepositoryTest.java +++ b/src/test/java/io/spring/infrastructure/comment/MyBatisCommentRepositoryTest.java @@ -10,6 +10,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; +/** + * Integration tests for the MyBatisCommentRepository. + * + *

Tests comment persistence operations including creating and fetching comments. + */ @Import({MyBatisCommentRepository.class}) public class MyBatisCommentRepositoryTest extends DbTestBase { @Autowired private CommentRepository commentRepository; diff --git a/src/test/java/io/spring/infrastructure/favorite/MyBatisArticleFavoriteRepositoryTest.java b/src/test/java/io/spring/infrastructure/favorite/MyBatisArticleFavoriteRepositoryTest.java index bd0f9db0b..ed67532c3 100644 --- a/src/test/java/io/spring/infrastructure/favorite/MyBatisArticleFavoriteRepositoryTest.java +++ b/src/test/java/io/spring/infrastructure/favorite/MyBatisArticleFavoriteRepositoryTest.java @@ -9,6 +9,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; +/** + * Integration tests for the MyBatisArticleFavoriteRepository. + * + *

Tests article favorite operations including saving and removing favorites. + */ @Import({MyBatisArticleFavoriteRepository.class}) public class MyBatisArticleFavoriteRepositoryTest extends DbTestBase { @Autowired private ArticleFavoriteRepository articleFavoriteRepository; diff --git a/src/test/java/io/spring/infrastructure/service/DefaultJwtServiceTest.java b/src/test/java/io/spring/infrastructure/service/DefaultJwtServiceTest.java index 12929118a..95d100d02 100644 --- a/src/test/java/io/spring/infrastructure/service/DefaultJwtServiceTest.java +++ b/src/test/java/io/spring/infrastructure/service/DefaultJwtServiceTest.java @@ -7,6 +7,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +/** + * Unit tests for the DefaultJwtService. + * + *

Tests JWT token generation and parsing, including handling of + * invalid and expired tokens. + */ public class DefaultJwtServiceTest { private JwtService jwtService; diff --git a/src/test/java/io/spring/infrastructure/user/MyBatisUserRepositoryTest.java b/src/test/java/io/spring/infrastructure/user/MyBatisUserRepositoryTest.java index 39876111c..905100e16 100644 --- a/src/test/java/io/spring/infrastructure/user/MyBatisUserRepositoryTest.java +++ b/src/test/java/io/spring/infrastructure/user/MyBatisUserRepositoryTest.java @@ -12,6 +12,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; +/** + * Integration tests for the MyBatisUserRepository. + * + *

Tests user CRUD operations and follow relationship management, + * including saving, updating, and fetching users, as well as + * creating and removing follow relationships. + */ @Import(MyBatisUserRepository.class) public class MyBatisUserRepositoryTest extends DbTestBase { @Autowired private UserRepository userRepository;