Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : expose endpoint to save post along with details, comments and tags #1647

Merged
merged 3 commits into from
Jan 14, 2025

Conversation

rajadilipkolli
Copy link
Owner

@rajadilipkolli rajadilipkolli commented Jan 14, 2025

Summary by CodeRabbit

  • New Features

    • Added MapStruct for object mapping
    • Introduced new post creation functionality with support for comments and tags
    • Created comprehensive API for post management
    • Added centralized exception handling for validation and duplicate entries
    • Introduced new data structures for post and comment requests
  • Improvements

    • Enhanced project build configuration
    • Added validation for post requests
    • Implemented repository methods for tag lookup
    • Improved handling of publication timestamps
  • Technical Updates

    • Updated date-time handling from OffsetDateTime to LocalDateTime
    • Added build information generation
    • Integrated MapStruct annotation processing
    • Expanded repository functionality for post and tag lookups

@rajadilipkolli rajadilipkolli self-assigned this Jan 14, 2025
Copy link
Contributor

coderabbitai bot commented Jan 14, 2025

Walkthrough

This pull request introduces significant enhancements to a Spring Boot JPA project by integrating MapStruct for object mapping, adding new request and response models, and implementing a post creation workflow. Changes include modifications to the Maven configuration, updates to entity classes, the creation of mapper interfaces, and the establishment of a new REST API endpoint for post creation with validation and transactional support.

Changes

File Change Summary
pom.xml Added MapStruct dependency, version property, and configuration for annotation processing; added Spring Boot validation dependency; updated spring-boot-maven-plugin for build-info; added maven-compiler-plugin for annotation processing.
PostComment.java Changed publishedAt field from OffsetDateTime to LocalDateTime.
PostMapper.java New MapStruct mapper interface for converting request models to entities.
PostMapperDecorator.java Decorator class for advanced mapping logic with tag and comment processing.
*Request.java Added new request models: PostRequest, PostCommentRequest, TagRequest.
PostResponse.java Placeholder response model.
TagRepository.java Added findByTagName and findByTagNameIn methods.
PostService.java New service interface for post creation.
JPAPostServiceImpl.java Service implementation for post creation.
PostAPI.java OpenAPI-defined interface for post-related operations.
PostController.java REST controller implementing post creation endpoint.
PostControllerIntTest.java Integration test for post creation functionality.
GlobalExceptionHandler.java Centralized exception handling with methods for validation and duplicate entry exceptions.
PostAlreadyExistsException.java New exception class for handling duplicate post titles.
PostCommentRepository.java New repository interface for PostComment entity.
PostRepository.java Added findByTitleIgnoreCase method.

Sequence Diagram

sequenceDiagram
    Client->>PostController: createPostByUserName(postRequest, userName)
    PostController->>PostService: createPost(postRequest, userName)
    PostService->>PostMapper: Convert request to entity
    PostMapper->>PostMapperDecorator: Process tags and comments
    PostMapperDecorator->>TagRepository: Check existing tags
    PostService->>PostRepository: Save post entity
    PostRepository-->>PostService: Saved post
    PostController-->>Client: 201 Created with Location header
Loading

Possibly related PRs

Suggested labels

component: r2dbc

Poem

🐰 Mapping magic, MapStruct's delight,
Transforming requests with coding might!
From request to entity, smooth as can be,
A rabbit's dance of technology,
Creating posts with elegant grace! 🌟


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9b5030 and ea87343.

📒 Files selected for processing (4)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/config/GlobalExceptionHandler.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/exception/PostAlreadyExistsException.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/repository/TagRepository.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/service/impl/JPAPostServiceImpl.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/exception/PostAlreadyExistsException.java
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/repository/TagRepository.java
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/service/impl/JPAPostServiceImpl.java
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/config/GlobalExceptionHandler.java
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Codacy Static Code Analysis

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added type: dependency-upgrade Pull requests that update a dependency file component: jpa-jooq labels Jan 14, 2025
public abstract class PostMapperDecorator implements PostMapper {

@Autowired
private TagRepository tagRepository;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai, how to use constructor injection instead of field injection in the Decorator of PostMapper defined using MapStruct

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rajadilipkolli Here's how to implement constructor injection in your MapStruct decorator:

public abstract class PostMapperDecorator implements PostMapper {

    private final PostMapper delegate;
    private final TagRepository tagRepository;

    protected PostMapperDecorator(PostMapper delegate, TagRepository tagRepository) {
        this.delegate = delegate;
        this.tagRepository = tagRepository;
    }
}

Remove the @Autowired field injection and use the constructor injection instead. MapStruct will automatically wire the delegate implementation and the repository through the constructor.

🔗 Analysis chain
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Find PostMapper interface
ast-grep --pattern 'interface PostMapper {
  $$$
}'

# Find MapStruct configuration for PostMapper
rg -l "@Mapper" | xargs rg "PostMapper"

Length of output: 770


✏️ Learnings added
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1647
File: jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapperDecorator.java:16-16
Timestamp: 2025-01-14T05:18:20.953Z
Learning: In MapStruct decorators, use constructor injection instead of field injection by adding constructor parameters for both the delegate mapper and any required dependencies. MapStruct will automatically wire the delegate implementation.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai, receiving error as below, please fix

Specified decorator type has no default constructor nor a constructor with a single parameter accepting the decorated mapper type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rajadilipkolli Here's the corrected constructor pattern for the MapStruct decorator:

public abstract class PostMapperDecorator implements PostMapper {

    private final PostMapper delegate;
    private final TagRepository tagRepository;

    @Autowired
    protected PostMapperDecorator(PostMapper delegate) {
        this.delegate = delegate;
        this.tagRepository = null; // Will be injected by Spring
    }

    @Autowired
    public void setTagRepository(TagRepository tagRepository) {
        this.tagRepository = tagRepository;
    }
}

This follows MapStruct's requirement of having a constructor with a single parameter for the delegate mapper, while still allowing Spring to inject the TagRepository through setter injection.


✏️ Learnings added
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1647
File: jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapperDecorator.java:16-16
Timestamp: 2025-01-14T05:26:52.862Z
Learning: In MapStruct decorators, the constructor must either be a default constructor or have a single parameter accepting the delegate mapper type. Additional dependencies should be injected through setter injection or field injection.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (10)
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/repository/TagRepository.java (1)

9-9: LGTM! Method follows Spring Data JPA conventions.

The findByTagName method is well-designed:

  • Uses Optional for null-safety
  • Follows Spring Data JPA naming conventions for automatic query generation
  • Clear and focused purpose aligned with the domain model

Consider adding a database index on the tag_name column if you expect frequent queries, especially if the tags table grows large:

CREATE INDEX idx_tag_name ON tags(tag_name);
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/web/controller/PostAPI.java (1)

14-32: Consider adding security and rate limiting documentation.

As this API handles user-generated content, consider documenting:

  1. Authentication requirements using @SecurityRequirement
  2. Rate limiting constraints
  3. Maximum request size limits

Example addition:

@SecurityRequirement(name = "bearer-jwt")
@Tag(name = "Post API")
interface PostAPI {
    // existing code
}

Also consider adding rate limiting metadata:

@RateLimiter(name = "post-creation")
ResponseEntity<PostResponse> createPostByUserName(...)
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/service/PostService.java (1)

5-8: Add documentation and validation constraints.

The interface would benefit from:

  1. Javadoc describing the method's purpose, parameters, and behavior
  2. @NotNull constraints on parameters
  3. @Transactional annotation if implementing classes need transaction management
 public interface PostService {
+    /**
+     * Creates a new post for the specified user.
+     *
+     * @param postRequest the post details to create
+     * @param userName the name of the user creating the post
+     * @return the created post response
+     * @throws IllegalArgumentException if the request is invalid
+     */
+    @Transactional
+    PostResponse createPost(@NotNull PostRequest postRequest, @NotNull String userName);
 }
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/model/request/PostRequest.java (1)

7-13: Add additional validation constraints for better data integrity.

Consider adding the following validations:

  • Size constraints for title and content to prevent extremely long inputs
  • @NotNull for the lists to prevent null values (if required)
  • @Valid for nested objects to trigger validation of PostCommentRequest and TagRequest
 public record PostRequest(
-        @NotBlank(message = "Title of post is mandatory") String title,
-        @NotBlank(message = "Content of post can't be Blank") String content,
+        @NotBlank(message = "Title of post is mandatory")
+        @Size(max = 255, message = "Title must not exceed 255 characters")
+        String title,
+        @NotBlank(message = "Content of post can't be Blank")
+        @Size(max = 10000, message = "Content must not exceed 10000 characters")
+        String content,
         boolean published,
         LocalDateTime publishedAt,
+        @Valid
         List<PostCommentRequest> comments,
+        @Valid
         List<TagRequest> tags) {}
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/service/impl/JPAPostServiceImpl.java (1)

22-27: Add error handling and logging for better observability.

The service method should:

  1. Validate the userName parameter
  2. Handle potential repository exceptions
  3. Add logging for debugging and monitoring
+    private static final Logger log = LoggerFactory.getLogger(JPAPostServiceImpl.class);
+
     @Override
     @Transactional
     public void createPost(PostRequest postRequest, String userName) {
+        if (StringUtils.isBlank(userName)) {
+            throw new IllegalArgumentException("Username cannot be blank");
+        }
+        log.debug("Creating post with title '{}' for user '{}'", postRequest.title(), userName);
         Post post = this.postMapper.postRequestToEntity(postRequest, userName);
-        this.postRepository.save(post);
+        try {
+            post = this.postRepository.save(post);
+            log.info("Successfully created post with id {} for user '{}'", post.getId(), userName);
+        } catch (DataAccessException e) {
+            log.error("Failed to create post for user '{}': {}", userName, e.getMessage());
+            throw new ServiceException("Failed to create post", e);
+        }
     }
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/web/controller/PostController.java (1)

23-35: Return created resource details in the response.

Consider returning the created post details in the response body for better client experience.

     @PostMapping("/{user_name}/posts/")
     @Override
-    public ResponseEntity<Object> createPostByUserName(
+    public ResponseEntity<PostResponse> createPostByUserName(
             @RequestBody @Valid PostRequest postRequest, @PathVariable("user_name") String userName) {
-        this.jpaPostService.createPost(postRequest, userName);
+        Post createdPost = this.jpaPostService.createPost(postRequest, userName);
+        PostResponse response = postMapper.toPostResponse(createdPost);
 
         URI location = ServletUriComponentsBuilder.fromCurrentRequest()
-                .path("{title}")
-                .buildAndExpand(postRequest.title())
+                .path("{id}")
+                .buildAndExpand(createdPost.getId())
                 .toUri();
 
-        return ResponseEntity.created(location).build();
+        return ResponseEntity.created(location).body(response);
     }
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapper.java (2)

11-15: Consider adding null value mapping strategy for collections.

The current null value check strategy might not handle empty collections appropriately.

 @Mapper(
         componentModel = MappingConstants.ComponentModel.SPRING,
         injectionStrategy = InjectionStrategy.CONSTRUCTOR,
-        nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
+        nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
+        collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)

18-18: Add mapping annotations for the main mapping method.

The postRequestToEntity method lacks mapping annotations which could lead to ambiguous mappings.

-    Post postRequestToEntity(PostRequest postRequest, String userName);
+    @Mapping(source = "userName", target = "details.createdBy")
+    @Mapping(source = "postRequest.comments", target = "comments")
+    @Mapping(source = "postRequest.tags", target = "tags")
+    Post postRequestToEntity(PostRequest postRequest, String userName);
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapperDecorator.java (2)

15-16: Consider using constructor injection instead of field injection.

Replace @Autowired field injection with constructor injection for better testability and to make dependencies explicit.

-    @Autowired
-    private TagRepository tagRepository;
+    private final TagRepository tagRepository;
+
+    public PostMapperDecorator(TagRepository tagRepository) {
+        this.tagRepository = tagRepository;
+    }

36-42: Consider adding batch processing for tag creation.

The current implementation performs database operations within a loop. For better performance with multiple tags, consider collecting new tags and performing a batch insert.

-        tagRequests.forEach(tagRequest -> {
-            // Check if the tag already exists
-            Optional<Tag> tagOptional = this.tagRepository.findByTagName(tagRequest.name());
-            Tag tag = tagOptional.orElseGet(() -> tagRequestToTag(tagRequest));
-            // Use the managed Tag entity to create and associate PostTag
-            post.addTag(tag);
-        });
+        Map<String, TagRequest> newTagRequests = tagRequests.stream()
+            .collect(Collectors.toMap(TagRequest::name, Function.identity()));
+        
+        // Find existing tags in a single query
+        List<Tag> existingTags = tagRepository.findByTagNameIn(newTagRequests.keySet());
+        Map<String, Tag> tagMap = existingTags.stream()
+            .collect(Collectors.toMap(Tag::getTagName, Function.identity()));
+        
+        // Create new tags in batch
+        List<Tag> newTags = newTagRequests.entrySet().stream()
+            .filter(e -> !tagMap.containsKey(e.getKey()))
+            .map(e -> tagRequestToTag(e.getValue()))
+            .collect(Collectors.toList());
+        if (!newTags.isEmpty()) {
+            tagRepository.saveAll(newTags);
+            newTags.forEach(tag -> tagMap.put(tag.getTagName(), tag));
+        }
+        
+        // Associate all tags with the post
+        tagRequests.forEach(tagRequest -> post.addTag(tagMap.get(tagRequest.name())));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fce5d32 and bf8cb6c.

📒 Files selected for processing (14)
  • jpa/boot-jpa-jooq-sample/pom.xml (3 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/entities/PostComment.java (3 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapper.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapperDecorator.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/model/request/PostCommentRequest.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/model/request/PostRequest.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/model/request/TagRequest.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/model/response/PostResponse.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/repository/TagRepository.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/service/PostService.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/service/impl/JPAPostServiceImpl.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/web/controller/PostAPI.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/web/controller/PostController.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/test/java/com/example/learning/web/controller/PostControllerIntTest.java (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/model/response/PostResponse.java
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/model/request/TagRequest.java
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/model/request/PostCommentRequest.java
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (4)
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/repository/TagRepository.java (1)

7-10: Verify error handling in the service layer.

Since this repository is used for tag existence checks during post creation, ensure proper error handling is implemented in the service layer when tag lookup fails.

jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/web/controller/PostAPI.java (1)

1-13: LGTM! Well-organized imports.

The package structure and imports are clean and follow best practices.

jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/service/PostService.java (1)

3-3: Verify PostRequest model validation.

Ensure that the PostRequest model has proper validation constraints for required fields and data integrity.

✅ Verification successful

PostRequest model has proper validation constraints

The PostRequest model includes appropriate validation:

  • Title: @notblank with mandatory validation
  • Content: @notblank validation
  • Optional fields (published, publishedAt) correctly lack constraints
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check PostRequest class for validation annotations
ast-grep --pattern 'class PostRequest {
  $$$
  @$_($$$)
  private $_ $_;
  $$$
}'

Length of output: 84


Script:

#!/bin/bash
# Find PostRequest.java file
fd PostRequest.java

# Examine content for validation annotations
rg -A 2 "@(NotNull|NotEmpty|NotBlank|Size|Min|Max|Pattern|Email|Valid)" --type java

Length of output: 65876

jpa/boot-jpa-jooq-sample/pom.xml (1)

141-147: Add Lombok's annotation processor to avoid conflicts.

If the project uses Lombok, its annotation processor should be added to avoid conflicts with MapStruct.

                     <annotationProcessorPaths>
                     <path>
                         <groupId>org.mapstruct</groupId>
                         <artifactId>mapstruct-processor</artifactId>
                         <version>${org.mapstruct.version}</version>
                     </path>
+                    <path>
+                        <groupId>org.projectlombok</groupId>
+                        <artifactId>lombok</artifactId>
+                        <version>${lombok.version}</version>
+                    </path>
+                    <path>
+                        <groupId>org.projectlombok</groupId>
+                        <artifactId>lombok-mapstruct-binding</artifactId>
+                        <version>0.2.0</version>
+                    </path>
                     </annotationProcessorPaths>

Comment on lines +45 to +53
this.mockMvcTester
.post()
.uri("/api/users/{user_name}/posts/", "junit")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(postRequest))
.assertThat()
.hasStatus(HttpStatus.CREATED)
.hasHeader(HttpHeaders.LOCATION, "http://localhost/api/users/junit/posts/newPostTitle");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add assertions for the created entity.

The test only verifies the HTTP response. Add assertions to verify that the post and its associated entities were correctly persisted in the database.

                .hasStatus(HttpStatus.CREATED)
-                .hasHeader(HttpHeaders.LOCATION, "http://localhost/api/users/junit/posts/newPostTitle");
+                .hasHeader(HttpHeaders.LOCATION, "http://localhost/api/users/junit/posts/newPostTitle")
+                .andDo(result -> {
+                    // Verify post was created
+                    Post savedPost = postRepository.findByTitleAndUserName("newPostTitle", "junit")
+                        .orElseThrow();
+                    assertThat(savedPost.getComments()).hasSize(2);
+                    assertThat(savedPost.getTags()).hasSize(3);
+                });

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapperDecorator.java (2)

15-19: 🛠️ Refactor suggestion

Refactor dependency injection pattern for MapStruct compatibility.

The current field injection pattern is not compatible with MapStruct's requirements. MapStruct decorators must have either a default constructor or a constructor with a single parameter accepting the delegate mapper.

Replace the field injection with this pattern:

 public abstract class PostMapperDecorator implements PostMapper {
-    @Autowired
-    private TagRepository tagRepository;
+    private final PostMapper delegate;
+    private TagRepository tagRepository;
+
+    @Autowired
+    protected PostMapperDecorator(PostMapper delegate) {
+        this.delegate = delegate;
+    }
+
+    @Autowired
+    public void setTagRepository(TagRepository tagRepository) {
+        this.tagRepository = tagRepository;
+    }

59-65: 🛠️ Refactor suggestion

Add validation and ordering for comments.

The current implementation lacks several important validations:

  1. No validation of comment content
  2. No chronological ordering of comments
  3. Missing transaction boundary

Consider this improved implementation:

+    @Transactional
     private void addPostCommentsToPost(List<PostCommentRequest> comments, Post post) {
         if (comments == null) {
             return;
         }
+        
+        // Validate comments
+        comments.forEach(comment -> {
+            if (comment.content() == null || comment.content().trim().isEmpty()) {
+                throw new IllegalArgumentException("Comment content cannot be empty");
+            }
+            if (comment.publishedAt() != null && comment.publishedAt().isAfter(LocalDateTime.now())) {
+                throw new IllegalArgumentException("Comment cannot be published in the future");
+            }
+        });
+        
+        // Sort comments by publishedAt before adding
+        comments.stream()
+               .sorted(Comparator.comparing(PostCommentRequest::publishedAt))
+               .forEach(comment -> post.addComment(postCommentRequestToPostComment(comment)));
-        comments.forEach(comment -> post.addComment(postCommentRequestToPostComment(comment)));
jpa/boot-jpa-jooq-sample/src/test/java/com/example/learning/web/controller/PostControllerIntTest.java (1)

22-58: 🛠️ Refactor suggestion

Enhance test coverage with additional assertions.

The test should verify all aspects of the created post:

  1. Post entity and its attributes
  2. Associated comments
  3. All associated tags

Add these assertions after the HTTP response check:

                .hasHeader(HttpHeaders.LOCATION, "http://localhost/api/users/junit/posts/newPostTitle");
+
+        // Verify post and its relationships
+        assertThat(postRepository.findByTitleAndUserName("newPostTitle", "junit"))
+                .isPresent()
+                .get()
+                .satisfies(savedPost -> {
+                    assertThat(savedPost.getContent()).isEqualTo("newPostContent");
+                    assertThat(savedPost.getIsPublished()).isTrue();
+                    assertThat(savedPost.getPublishedOn()).isEqualTo(LocalDateTime.parse("2025-01-15T10:00:00"));
+                    
+                    // Verify comments
+                    assertThat(savedPost.getComments())
+                            .hasSize(2)
+                            .extracting("title", "content")
+                            .containsExactly(
+                                tuple("commentTitle1", "Nice Post1"),
+                                tuple("commentTitle2", "Nice Post2")
+                            );
+                    
+                    // Verify tags
+                    assertThat(savedPost.getTags())
+                            .hasSize(3)
+                            .extracting("tagName")
+                            .containsExactlyInAnyOrder("junit", "spring", "Java");
+                });
🧹 Nitpick comments (9)
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/repository/TagRepository.java (1)

11-13: Consider adding parameter annotations and documentation.

The finder methods are well-designed and follow Spring Data JPA conventions. Consider these enhancements:

  • Add @NonNull annotations to parameters to enforce null safety
  • Add Javadoc to document the methods' purposes and behavior

Example enhancement:

/**
 * Finds a tag by its name.
 * @param tagName the name of the tag to find
 * @return the tag if found, empty optional otherwise
 */
Optional<Tag> findByTagName(@NonNull String tagName);

/**
 * Finds all tags whose names are in the given set.
 * @param tagNameSet set of tag names to find
 * @return list of matching tags
 */
List<Tag> findByTagNameIn(@NonNull Set<String> tagNameSet);
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapperDecorator.java (1)

33-57: Consider adding transaction boundary and tag validation.

While the implementation is efficient with batch operations, consider these improvements:

  1. Add transaction boundary to ensure atomicity
  2. Add validation for tag names (e.g., length, format)

Example implementation:

+    @Transactional
     private void addPostTagsToPost(List<TagRequest> tagRequests, Post post) {
         if (tagRequests == null) {
             return;
         }
+        
+        // Validate tag names
+        tagRequests.forEach(tag -> {
+            if (tag.name() == null || tag.name().trim().isEmpty()) {
+                throw new IllegalArgumentException("Tag name cannot be empty");
+            }
+            if (tag.name().length() > 50) {  // adjust max length as needed
+                throw new IllegalArgumentException("Tag name too long");
+            }
+        });
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/exception/PostAlreadyExistsException.java (1)

6-17: Add serialization support to the exception class.

While the implementation is good, consider adding serialVersionUID for proper serialization support, as this is a serializable class extending RuntimeException.

 public class PostAlreadyExistsException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
 
     private static final HttpStatus httpStatus = HttpStatus.CONFLICT;
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/config/GlobalExceptionHandler.java (3)

50-50: Consider externalizing the error type URI.

The hardcoded URI should be externalized to a configuration property for better maintainability and flexibility.


52-52: Consider using ISO-8601 formatted string for timestamp.

While Instant works, using an ISO-8601 formatted string might be more widely compatible across different JSON parsers and clients.

-        problemDetail.setProperty("timestamp", Instant.now());
+        problemDetail.setProperty("timestamp", Instant.now().toString());

30-41: Consider adding validation error count to the response.

For better error reporting, consider including the total count of validation errors in the response.

         List<ApiValidationError> validationErrorsList = methodArgumentNotValidException.getAllErrors().stream()
                 // ... existing mapping code ...
                 .toList();
         problemDetail.setProperty("violations", validationErrorsList);
+        problemDetail.setProperty("totalViolations", validationErrorsList.size());
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/service/impl/JPAPostServiceImpl.java (1)

30-39: Consider enhancing error handling and logging.

While the implementation is good, consider these improvements:

  1. Validate input parameters (postRequest and userName) for null values
  2. Add debug logging for the case when post already exists
  3. Add trace logging for the saved post details
     @Transactional
     public void createPost(PostRequest postRequest, String userName) {
+        Objects.requireNonNull(postRequest, "PostRequest must not be null");
+        Objects.requireNonNull(userName, "UserName must not be null");
+
         Optional<Post> byTitleIgnoreCase = this.postRepository.findByTitleIgnoreCase(postRequest.title());
         if (byTitleIgnoreCase.isPresent()) {
+            log.debug("Post with title '{}' already exists", postRequest.title());
             throw new PostAlreadyExistsException(postRequest.title());
         } else {
             log.debug("Creating post with title '{}' for user '{}'", postRequest.title(), userName);
             Post post = this.postMapper.postRequestToEntity(postRequest, userName);
-            this.postRepository.save(post);
+            Post savedPost = this.postRepository.save(post);
+            log.trace("Created post with id: {}", savedPost.getId());
         }
     }
jpa/boot-jpa-jooq-sample/src/test/java/com/example/learning/web/controller/PostControllerIntTest.java (2)

60-113: Improve test maintainability and coverage.

  1. Extract duplicate assertion logic into a helper method
  2. Add tests for other validation scenarios:
    • Invalid dates (future/past)
    • Null fields
    • Invalid comment data
    • Invalid tag data
+    private void assertBadRequest(PostRequest request, String expectedViolation) throws Exception {
+        this.mockMvcTester
+                .post()
+                .uri("/api/users/{user_name}/posts/", "junit")
+                .contentType(MediaType.APPLICATION_JSON)
+                .accept(MediaType.APPLICATION_JSON)
+                .content(objectMapper.writeValueAsString(request))
+                .assertThat()
+                .hasStatus(HttpStatus.BAD_REQUEST)
+                .bodyJson()
+                .convertTo(ProblemDetail.class)
+                .satisfies(problemDetail -> {
+                    assertThat(problemDetail.getTitle()).isEqualTo("Constraint Violation");
+                    assertThat(problemDetail.getStatus()).isEqualTo(400);
+                    assertThat(problemDetail.getDetail()).isEqualTo(expectedViolation);
+                    assertThat(problemDetail.getInstance()).isEqualTo(URI.create("/api/users/junit/posts/"));
+                });
+    }

     @Test
     void shouldFailForInvalidInput() throws Exception {
-        // Test with blank title
-        PostRequest invalidRequest = new PostRequest(
+        // Test various validation scenarios
+        assertBadRequest(
+            new PostRequest("", "content", true, LocalDateTime.parse("2025-01-15T10:00:00"), List.of(), List.of()),
+            "Title must not be blank"
+        );
+
+        assertBadRequest(
+            new PostRequest("title", "", true, LocalDateTime.parse("2025-01-15T10:00:00"), List.of(), List.of()),
+            "Content must not be blank"
+        );
+
+        assertBadRequest(
+            new PostRequest("title", "content", true, null, List.of(), List.of()),
+            "Published date must not be null"
+        );

115-164: Enhance duplicate title test assertions.

The test should verify the actual properties in the problem detail instead of just checking the size.

                     assertThat(problemDetail.getProperties())
                             .isNotNull()
                             .isNotEmpty()
-                            .hasSize(2);
+                            .containsExactlyInAnyOrderEntriesOf(Map.of(
+                                "timestamp", LocalDateTime.now().toString(),
+                                "errorCategory", "DATA_INTEGRITY_VIOLATION"
+                            ));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf8cb6c and a9b5030.

📒 Files selected for processing (12)
  • jpa/boot-jpa-jooq-sample/pom.xml (4 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/config/GlobalExceptionHandler.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/exception/PostAlreadyExistsException.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapper.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapperDecorator.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/repository/PostCommentRepository.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/repository/PostRepository.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/repository/TagRepository.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/service/impl/JPAPostServiceImpl.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/web/controller/PostAPI.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/web/controller/PostController.java (1 hunks)
  • jpa/boot-jpa-jooq-sample/src/test/java/com/example/learning/web/controller/PostControllerIntTest.java (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/repository/PostCommentRepository.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/web/controller/PostAPI.java
  • jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/web/controller/PostController.java
  • jpa/boot-jpa-jooq-sample/pom.xml
🧰 Additional context used
📓 Learnings (2)
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapperDecorator.java (2)
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1647
File: jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapperDecorator.java:16-16
Timestamp: 2025-01-14T05:26:53.024Z
Learning: In MapStruct decorators, the constructor must either be a default constructor or have a single parameter accepting the delegate mapper type. Additional dependencies should be injected through setter injection or field injection.
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1647
File: jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapperDecorator.java:16-16
Timestamp: 2025-01-14T05:18:21.158Z
Learning: In MapStruct decorators, use constructor injection instead of field injection by adding constructor parameters for both the delegate mapper and any required dependencies. MapStruct will automatically wire the delegate implementation.
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapper.java (2)
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1647
File: jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapperDecorator.java:16-16
Timestamp: 2025-01-14T05:26:53.024Z
Learning: In MapStruct decorators, the constructor must either be a default constructor or have a single parameter accepting the delegate mapper type. Additional dependencies should be injected through setter injection or field injection.
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1647
File: jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapperDecorator.java:16-16
Timestamp: 2025-01-14T05:18:21.158Z
Learning: In MapStruct decorators, use constructor injection instead of field injection by adding constructor parameters for both the delegate mapper and any required dependencies. MapStruct will automatically wire the delegate implementation.
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (10)
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/repository/TagRepository.java (3)

4-6: LGTM! Imports are correctly organized.

The added imports are necessary for the new method signatures and follow standard Java conventions.


9-10: LGTM! Repository interface is properly defined.

The interface correctly extends JpaRepository with appropriate entity type and ID parameters.


11-13: Verify the integration with PostMapperDecorator.

Let's verify the usage of these repository methods in the PostMapperDecorator class.

✅ Verification successful

✓ Repository methods are properly integrated

The methods are correctly used in the codebase:

  • findByTagNameIn is efficiently used in PostMapperDecorator for batch tag operations
  • findByTagName is properly utilized in tests for verification
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for usage of the new repository methods
echo "Searching for findByTagName usage:"
rg "findByTagName" -A 3

echo -e "\nSearching for findByTagNameIn usage:"
rg "findByTagNameIn" -A 3

Length of output: 5864

jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapperDecorator.java (1)

20-31: LGTM! Well-structured mapping implementation.

The method follows good practices with proper null checking and clear separation of concerns for mapping child entities.

jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/repository/PostRepository.java (1)

9-9: LGTM! Well-designed repository method.

The method follows Spring Data JPA naming conventions and appropriately uses Optional for the return type. Case-insensitive search is a good practice for title comparisons.

jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/service/impl/JPAPostServiceImpl.java (1)

23-26: LGTM! Good use of constructor injection.

Constructor injection is the recommended approach for required dependencies.

jpa/boot-jpa-jooq-sample/src/test/java/com/example/learning/web/controller/PostControllerIntTest.java (1)

20-20: Add missing test scenarios.

Based on previous review comments, add test cases for:

  1. Error scenarios (invalid user, malformed JSON)
  2. Duplicate tag handling

Let's verify the current test coverage:

#!/bin/bash
# Check test coverage for PostController
find . -type f -name "PostController.java" -exec echo "Found PostController at: {}" \;

# Look for error handling methods
ast-grep --pattern 'class PostController {
  $$$
  @ExceptionHandler($_)
  $$$
}'

# Look for tag handling methods
ast-grep --pattern 'class PostController {
  $$$
  $_(Tag $_) {
    $$$
  }
  $$$
}'
jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapper.java (3)

1-16: LGTM! Well-organized imports.

The package structure and imports are clean and well-organized, following Java conventions.


27-45: LGTM! Well-structured mapping configurations.

The mapping methods are well-defined with:

  • Appropriate ignoring of JPA-managed fields
  • Clear and consistent field mappings
  • Proper handling of relationships

25-25: Document the base mapping method and verify its implementation.

The postRequestToEntity method lacks specific mapping configurations. Since it's likely implemented in the decorator, let's verify its implementation and suggest adding documentation.

Let's check the decorator's implementation:

Consider adding Javadoc to clarify the method's purpose:

+    /**
+     * Maps a PostRequest to a Post entity, handling nested comments and tags.
+     * @param postRequest the request containing post data
+     * @param userName the creator's username
+     * @return fully populated Post entity
+     */
     Post postRequestToEntity(PostRequest postRequest, String userName);
✅ Verification successful

Documentation would improve code clarity, but implementation follows best practices

The postRequestToEntity method is properly implemented as part of a well-structured MapStruct mapper that:

  • Uses decorator pattern for complex mapping logic
  • Follows consistent mapping patterns
  • Properly handles nested objects

Consider adding the suggested documentation to improve clarity:

/**
 * Maps a PostRequest to a Post entity, handling nested comments and tags.
 * @param postRequest the request containing post data
 * @param userName the creator's username
 * @return fully populated Post entity
 */
Post postRequestToEntity(PostRequest postRequest, String userName);
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check the implementation of postRequestToEntity in the decorator
ast-grep --pattern 'class PostMapperDecorator {
  $$$
  public Post postRequestToEntity($$$) {
    $$$
  }
  $$$
}'

Length of output: 117


Script:

#!/bin/bash
# Find all files related to PostMapper
fd -t f -e java . | rg -l "PostMapper"

# Search for the implementation pattern with different class names
ast-grep --pattern 'Post postRequestToEntity(PostRequest $_, String $_) {
  $$$
}'

# Look for MapStruct configuration
rg -l "@Mapper|@DecoratedWith" --type java

Length of output: 1955


Script:

#!/bin/bash
# Check the content of PostMapper.java
cat "jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapper.java"

# Search for PostMapperImpl or similar implementation files
fd -t f -e java . | rg -l "PostMapper(Impl|Decorator)"

Length of output: 2106

Comment on lines +17 to +22
@Mapper(
componentModel = MappingConstants.ComponentModel.SPRING,
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
@DecoratedWith(PostMapperDecorator.class)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Field injection in decorator contradicts mapper's constructor injection pattern

The PostMapperDecorator implementation has several issues that contradict the best practices set in the mapper configuration:

  • Uses field injection (@Autowired private TagRepository) instead of constructor injection
  • Missing delegate mapper injection which is typically required for MapStruct decorators
  • Could lead to testing difficulties and potential runtime issues

Suggested improvements:

  • Replace field injection with constructor injection
  • Add delegate mapper as a constructor parameter
  • Example structure:
private final PostMapper delegate;
private final TagRepository tagRepository;

public PostMapperDecorator(PostMapper delegate, TagRepository tagRepository) {
    this.delegate = delegate;
    this.tagRepository = tagRepository;
}
🔗 Analysis chain

Verify PostMapperDecorator implementation based on learnings.

The mapper configuration follows best practices with constructor injection, null checking, and appropriate collection mapping strategy. However, based on the retrieved learnings, ensure that the PostMapperDecorator follows these guidelines:

  • Uses constructor injection for both delegate mapper and dependencies
  • Avoids field injection

Let's verify the decorator implementation:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check PostMapperDecorator implementation
ast-grep --pattern 'class PostMapperDecorator {
  $$$
  @Autowired
  $$$
}'

# Check for constructor
ast-grep --pattern 'class PostMapperDecorator {
  $$$
  PostMapperDecorator($_) {
    $$$
  }
  $$$
}'

Length of output: 182


Script:

#!/bin/bash
# Find decorator file
rg -l "PostMapperDecorator"

# Once found, let's see its implementation
rg -A 10 "class PostMapperDecorator"

Length of output: 1542


Script:

#!/bin/bash
# View complete decorator implementation
cat "jpa/boot-jpa-jooq-sample/src/main/java/com/example/learning/mapper/PostMapperDecorator.java"

Length of output: 2502

Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
3.9% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@rajadilipkolli rajadilipkolli merged commit 1e30470 into main Jan 14, 2025
7 of 8 checks passed
@rajadilipkolli rajadilipkolli deleted the createPost branch January 14, 2025 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: jpa-jooq type: dependency-upgrade Pull requests that update a dependency file
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant