Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.codesungrape.hmcts.BookAPI;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BookApiApplication {

public static void main(String[] args) {
SpringApplication.run(BookApiApplication.class, args);
}

}
27 changes: 27 additions & 0 deletions src/main/java/com/codesungrape/hmcts/BookAPI/dto/BookRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.codesungrape.hmcts.BookAPI.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
import lombok.Value;

/**
* DTO representing the required input for creating or replacing a Book resource.
* This class mirrors the OpenAPI 'BookInput' schema.
* @Value: Makes all fields 'final' (immutable), generates constructor, getters, and equals/hashCode/toString.
* @NotBlank: Enforces the required status from your OpenAPI schema. If the field is missing or an empty string, Spring will return a 400 Bad Request.
* @JsonProperty: Jackson library - maps snake_case JSON (HMCTS rules) to camelCase Java
*/
@Value
public class BookRequest {
@NotBlank(message= "Title is required") // enforces non-empty title requirement
@JsonProperty("title")
String title;

@NotBlank(message = "Synopsis is required")
@JsonProperty("synopsis")
String synopsis;

@NotBlank(message = "Author is required")
@JsonProperty("author")
String author;
}
70 changes: 70 additions & 0 deletions src/main/java/com/codesungrape/hmcts/BookAPI/entity/Book.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.codesungrape.hmcts.BookAPI.entity;

import jakarta.persistence.*;
import java.util.UUID;
import lombok.*;

/**
* JPA Entity representing the Book table in PostgreSQL.
* This holds the persisted state of the resource.
* HMCTS Rule Check: IDs must be opaque strings. Using UUID for distributed ID generation.
* @Entity: Marks the class as a JPA entity - tells hibernate to map Java classes to database tables.
* @Table: Defines which database table this entity maps to. HMCTS Naming: Lowercase, singular table name is common practice.
* Lombok annotations:
* @Getter: Automatically generates getters for all fields.
* @Setter: Automatically generates setters.
* @AllArgsConstructor: Generates a no-argument constructor (required by JPA).
* JPA needs to instantiate the entity using reflection. 'PROTECTED' prevents misuse.
* @Builder: Adds a builder pattern for clean object creation.
* You can do Book.builder().title("A").author("B").build();
*/
@Entity
@Table(name = "book")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // For JPA/Hibernate requirements
@AllArgsConstructor // For easy construction in tests
@Builder // For convenience in creating instances
public class Book {

@Id // Primary key of the table
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "id", nullable = false) // maps the field to a database column named 'id' + 'nullable =false' database column cannot be NULL.
private UUID id;

@Column(name = "title", nullable = false)
private String title;

@Column(name = "synopsis", nullable = false, columnDefinition = "TEXT")
private String synopsis;

@Column(name = "author", nullable = false)
private String author;

// Soft delete - makes DELETE operations idempotent (safe to repeat)
@Column(name = "deleted", nullable = false)
@Builder.Default
private boolean deleted = false;

@Column(name = "created_at", nullable = false)
private java.time.Instant createdAt;

@Column(name = "modified_at")
private java.time.Instant modifiedAt;

// --- Business Logic Helper ---
// HMCTS mandates business logic in services, but a setter hook is acceptable.
// Lifecycle callback - special method runs automatically before Hibernate persists a record in the database.
@PrePersist
protected void onCreate() {
this.createdAt = java.time.Instant.now();
}

// Lifecycle callback - special method runs automatically before Hibernate updates a record in the database.
@PreUpdate
protected void onUpdate() {
this.modifiedAt = java.time.Instant.now();
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.codesungrape.hmcts.BookAPI.repository;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

import com.codesungrape.hmcts.BookAPI.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
* Repository interface for Book Entity.
* Spring Data JPA automatically provides CRUD operations based on the Entity and ID type.
*/
@Repository
public interface BookRepository extends JpaRepository<Book, UUID> {

// Custom query to find books that have NOT been soft-deleted
List<Book> findAllByDeletedFalse();

// Custom query to find a specific, non-deleted book by ID.
Optional<Book> findByIdAndDeleteFalse(UUID id);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.codesungrape.hmcts.BookAPI.service;

import com.codesungrape.hmcts.BookAPI.dto.BookRequest;
import com.codesungrape.hmcts.BookAPI.repository.BookRepository;
import org.springframework.stereotype.Service; // Marks a class as a Service Layer component.
import lombok.RequiredArgsConstructor;

import com.codesungrape.hmcts.BookAPI.entity.Book;

/**
* Service layer responsible for all business logic related to the Book resource.
*/
@Service
@RequiredArgsConstructor // Lombok creates constructor for dependency injection
public class BookService {

// Create a field to store the repo
private final BookRepository bookRepository;

// 1. CREATE Operation (POST /books)
public Book createBook(BookRequest request) {
// Validation check for business rules (e.g., uniqueness, if required)
if (request == null) {
throw new NullPointerException("BookRequest cannot be null");
}

// Map DTO to Entity
Book newBook = Book.builder()
.title(request.getTitle())
.author(request.getAuthor())
.synopsis(request.getSynopsis())
// ID and created_at are auto-generated by JPA/DB
.build();

Book savedBook = bookRepository.save(newBook);

// Defensive check (even though it "shouldn't" happen aka follows JPA contract)
if (savedBook == null) {
throw new IllegalStateException("Failed to save book - repository returned null");
}

return savedBook;
}
}
3 changes: 3 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
spring:
application:
name: BookAPI
Loading