Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,15 @@ FRONT_URL=http://localhost:3000
POSTGRES_USER=inktrack_user
POSTGRES_PASSWORD=inktrack_password
POSTGRES_DB=inktrack_db

# Google Books API Configuration
# API key is optional but recommended for higher quotas and better rate limiting
# Get your free API key at: https://console.cloud.google.com/apis/credentials
GOOGLE_BOOKS_API_KEY=
GOOGLE_BOOKS_MAX_RESULTS=10
GOOGLE_BOOKS_TIMEOUT_MS=5000
# Cache Configuration (Caffeine - in-memory cache)
# Maximum number of search results to cache
GOOGLE_BOOKS_CACHE_MAX_SIZE=100
# Cache expiration time in minutes
GOOGLE_BOOKS_CACHE_EXPIRATION_MINUTES=5
85 changes: 80 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ practices.

- ✅ **Secure Authentication**: JWT with Access (15min) + Refresh (7 days) token pattern, BCrypt password hashing, CORS configuration
- ✅ **Book Management**: Full CRUD operations with category organization, supporting unlimited books per user
- ✅ **Google Books Integration**: Search books via Google Books API, automatic thumbnail fetching, and metadata
enrichment
- ✅ **Reading Progress**: Page-level tracking with automatic progress calculation, reading session history, and completion detection
- ✅ **Notes System**: Create, update, and delete notes per book with full audit trail (createdAt/updatedAt)
- ✅ **Advanced Analytics**:
Expand Down Expand Up @@ -276,21 +278,47 @@ graph TB
5. Gateway Implementation → Repository/Service
6. Response flows back through the layers

### Google Books Integration

The project integrates with Google Books API to provide enhanced book discovery and metadata:

**Architecture**:

- **Gateway Interface**: `GoogleBooksGateway` in core/gateway defines the contract
- **Implementation**: `GoogleBooksGatewayImpl` in infrastructure/gateway uses Spring's RestClient
- **Caching**: Responses are cached using Caffeine to reduce API calls (5-minute TTL)
- **Domain Models**: `GoogleBooksVolume`, `GoogleBooksSearchResponse` for API responses

**Use Cases**:

1. **Book Search**: Search for books by title, author, or ISBN via `GET /books/search`
2. **Thumbnail Fetching**: Automatically fetch book thumbnails when `googleBookId` is provided
3. **Metadata Enrichment**: Enhance book records with Google Books metadata

**Configuration**:

- API key configurable via `GOOGLE_BOOKS_API_KEY` environment variable
- Cache size and timeout configurable
- Respects API rate limits through caching strategy

### Project Structure

```
src/main/java/com/inktrack/
├── core/ # Domain logic
│ ├── domain/ # Domain entities (User, Book, Category, Note, ReadingSession)
│ ├── domain/ # Domain entities (User, Book, Category, Note, ReadingSession, GoogleBooksVolume)
│ ├── exception/ # Custom exceptions
│ ├── gateway/ # Gateway interfaces
│ ├── gateway/ # Gateway interfaces (UserGateway, BookGateway, GoogleBooksGateway)
│ ├── usecases/ # Use cases (business logic)
│ │ └── book/ # Book-specific use cases (SearchBooksUseCase, etc.)
│ └── utils/ # Utility classes
├── infrastructure/ # Implementation details
│ ├── config/ # Configuration classes (GoogleBooksConfig, CacheConfig)
│ ├── controller/ # REST API controllers
│ ├── dto/ # Data Transfer Objects
│ │ └── book/ # Book DTOs (BookSearchResponse, etc.)
│ ├── entity/ # JPA entities
│ ├── gateway/ # Gateway implementations
│ ├── gateway/ # Gateway implementations (GoogleBooksGatewayImpl)
│ ├── mapper/ # DTO/Domain mappers
│ ├── persistence/ # Repository interfaces
│ ├── security/ # Security configuration
Expand Down Expand Up @@ -517,6 +545,12 @@ cp .env.example .env
- `POSTGRES_USER`: Database username (for production/Docker)
- `POSTGRES_PASSWORD`: Database password (for production/Docker)
- `POSTGRES_DB`: Database name (for production/Docker)
- `GOOGLE_BOOKS_API_KEY`: API key for Google Books API (optional, for enhanced search and thumbnail features)
- `GOOGLE_BOOKS_API_BASE_URL`: Google Books API base URL (default: https://www.googleapis.com/books/v1)
- `GOOGLE_BOOKS_MAX_RESULTS`: Maximum search results (default: 10)
- `GOOGLE_BOOKS_TIMEOUT_MS`: Request timeout in milliseconds (default: 5000)
- `GOOGLE_BOOKS_CACHE_MAX_SIZE`: Maximum cache entries (default: 100)
- `GOOGLE_BOOKS_CACHE_EXPIRATION_MINUTES`: Cache expiration in minutes (default: 5)

## 📚 API Documentation

Expand Down Expand Up @@ -637,7 +671,8 @@ Authorization: Bearer {accessToken}
"title": "Clean Code",
"author": "Robert C. Martin",
"totalPages": 464,
"categoryId": 1
"categoryId": 1,
"googleBookId": "p-cDwAAQBAJ"
}
```

Expand Down Expand Up @@ -675,6 +710,8 @@ Content-Type: application/json
}
```

**Note**: When `googleBookId` is provided, the system automatically fetches the book's thumbnail from Google Books API.

#### Get All Books

```http
Expand Down Expand Up @@ -702,6 +739,43 @@ Content-Type: application/json
}
```

#### Search Books (Google Books Integration)

**Request** (Search Books):

```http
GET /api/v1/books/search?q=clean+code
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

**Response** (200 OK):

```json
{
"success": true,
"message": "Operação realizada com sucesso.",
"data": {
"total": 128,
"items": [
{
"googleBookId": "p-cDwAAQBAJ",
"title": "Clean Code: A Handbook of Agile Software Craftsmanship",
"author": "Robert C. Martin",
"totalPages": 464,
"thumbnailUrl": "http://books.google.com/books/content?id=p-cDwAAQBAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api"
}
]
},
"errors": [],
"timestamp": "2024-01-15T10:35:00Z"
}
```

**Query Parameters:**

- `q` (required): Search query string (e.g., book title, author, ISBN)
- Returns up to 10 results with book metadata including thumbnails

#### Get Book by ID

```http
Expand All @@ -720,7 +794,8 @@ Authorization: Bearer {accessToken}
"title": "Clean Code: A Handbook of Agile Software Craftsmanship",
"author": "Robert C. Martin",
"totalPages": 464,
"categoryId": 1
"categoryId": 1,
"googleBookId": "p-cDwAAQBAJ"
}
```

Expand Down
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/inktrack/core/domain/Book.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public final class Book {
private final String author;
private final int totalPages;
private int pagesRead;
private String thumbnailUrl;
private String googleBookId;
private OffsetDateTime createdAt;
private OffsetDateTime updatedAt;

Expand All @@ -25,6 +27,8 @@ private Book(Builder builder) {
this.author = builder.author;
this.totalPages = builder.totalPages;
this.pagesRead = builder.pagesRead;
this.thumbnailUrl = builder.thumbnailUrl;
this.googleBookId = builder.googleBookId;
this.createdAt = builder.createdAt;
this.updatedAt = builder.updatedAt;
}
Expand Down Expand Up @@ -99,6 +103,14 @@ public OffsetDateTime getUpdatedAt() {
return updatedAt;
}

public String getThumbnailUrl() {
return thumbnailUrl;
}

public String getGoogleBookId() {
return googleBookId;
}

public static Builder builder() {
return new Builder();
}
Expand Down Expand Up @@ -127,6 +139,8 @@ public static final class Builder {
private String author;
private int totalPages;
private int pagesRead = 0;
private String thumbnailUrl;
private String googleBookId;
private OffsetDateTime createdAt;
private OffsetDateTime updatedAt;

Expand Down Expand Up @@ -178,6 +192,16 @@ public Builder updatedAt(OffsetDateTime updatedAt) {
return this;
}

public Builder thumbnailUrl(String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
return this;
}

public Builder googleBookId(String googleBookId) {
this.googleBookId = googleBookId;
return this;
}

public Book build() {
return new Book(this);
}
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/inktrack/core/gateway/GoogleBooksGateway.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.inktrack.core.gateway;

import com.inktrack.core.usecases.book.GoogleBooksVolume;
import com.inktrack.core.usecases.book.SearchBooksOutput;

import java.util.Optional;

public interface GoogleBooksGateway {

SearchBooksOutput searchBooks(String query);

Optional<GoogleBooksVolume> getVolumeById(String volumeId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public record BookModelInput(
String title,
String author,
int totalPages,
Long categoryId
Long categoryId,
String googleBookId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public record BookModelOutput(
int totalPages,
int pagesRead,
int progress,
String thumbnailUrl,
String googleBookId,
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,66 @@
import com.inktrack.core.domain.User;
import com.inktrack.core.gateway.BookGateway;
import com.inktrack.core.gateway.CategoryGateway;
import com.inktrack.core.gateway.GoogleBooksGateway;
import com.inktrack.core.usecases.category.CategoryOutput;
import com.inktrack.core.usecases.user.UserOutput;

import java.util.Optional;

public class CreateBookUseCaseImpl implements CreateBookUseCase {

private final BookGateway bookGateway;
private final CategoryGateway categoryGateway;
private final GoogleBooksGateway googleBooksGateway;

public CreateBookUseCaseImpl(BookGateway bookGateway, CategoryGateway categoryGateway) {
public CreateBookUseCaseImpl(
BookGateway bookGateway,
CategoryGateway categoryGateway,
GoogleBooksGateway googleBooksGateway
) {
this.bookGateway = bookGateway;
this.categoryGateway = categoryGateway;
this.googleBooksGateway = googleBooksGateway;
}

@Override
public BookModelOutput execute(BookModelInput modelInput, User currentUser) {
validateInput(modelInput, currentUser);
Category category = categoryGateway.getById(modelInput.categoryId())
.orElseThrow(() -> new IllegalArgumentException("Category not found with id: " + modelInput.categoryId()));

String thumbnailUrl = fetchThumbnailFromGoogleBooks(modelInput.googleBookId());
Book book = createBook(modelInput, currentUser, category, thumbnailUrl);
Book bookSaved = bookGateway.save(book);

return buildOutput(bookSaved);
}

private void validateInput(BookModelInput modelInput, User currentUser) {
if (currentUser == null) {
throw new IllegalArgumentException("User not logged in");
}

if(modelInput.totalPages() < 0) {
throw new IllegalArgumentException("The total pages must be greater than zero.");
}

if(modelInput.title().isBlank() || modelInput.author().isBlank()) {
throw new IllegalArgumentException("The title and author not can be black or null.");
}
}

Category category = categoryGateway.getById(modelInput.categoryId())
.orElseThrow(() -> new IllegalArgumentException("Category not found with id: " + modelInput.categoryId()));

Book book = Book.builder()
private Book createBook(BookModelInput modelInput, User currentUser, Category category, String thumbnailUrl) {
return Book.builder()
.user(currentUser)
.category(category)
.title(modelInput.title())
.author(modelInput.author())
.totalPages(modelInput.totalPages())
.thumbnailUrl(thumbnailUrl)
.googleBookId(modelInput.googleBookId())
.build();
}

Book bookSaved = bookGateway.save(book);
private BookModelOutput buildOutput(Book bookSaved) {
UserOutput userOutput = new UserOutput(
bookSaved.getUser().getId(),
bookSaved.getUser().getName(),
Expand All @@ -64,8 +85,24 @@ public BookModelOutput execute(BookModelInput modelInput, User currentUser) {
bookSaved.getTotalPages(),
bookSaved.getPagesRead(),
bookSaved.getProgress(),
bookSaved.getThumbnailUrl(),
bookSaved.getGoogleBookId(),
bookSaved.getCreatedAt(),
bookSaved.getUpdatedAt()
);
}

private String fetchThumbnailFromGoogleBooks(String googleBookId) {
if (googleBookId == null || googleBookId.isBlank()) {
return null;
}

try {
Optional<GoogleBooksVolume> volume = googleBooksGateway.getVolumeById(googleBookId);
return volume.map(GoogleBooksVolume::thumbnailUrl).orElse(null);
} catch (Exception e) {
System.err.println("Failed to fetch book thumbnail from Google Books: " + e.getMessage());
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public BookModelOutput execute(Long bookID, UUID userId) {
book.getTotalPages(),
book.getPagesRead(),
book.getProgress(),
book.getThumbnailUrl(),
book.getGoogleBookId(),
book.getCreatedAt(),
book.getUpdatedAt()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public PageResult<BookModelOutput> execute(UUID userId, GetBookFilter filter) {
b.getTotalPages(),
b.getPagesRead(),
b.getProgress(),
b.getThumbnailUrl(),
b.getGoogleBookId(),
b.getCreatedAt(),
b.getUpdatedAt()
);
Expand Down
Loading
Loading