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
43 changes: 35 additions & 8 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Project Overview

This is a RESTful Web Service proof-of-concept built with **Spring Boot 4** targeting **JDK 25 (LTS)**. The application demonstrates a clean, layered architecture implementing a CRUD API for managing books. It uses an in-memory H2 database for data persistence and includes comprehensive test coverage.
This is a RESTful Web Service proof-of-concept built with **Spring Boot 4** targeting **JDK 25 (LTS)**. The application demonstrates a clean, layered architecture implementing a CRUD API for managing books. It uses a **SQLite database** for runtime persistence (with a pre-seeded database in Docker) and **H2 in-memory** for fast test execution.

**Key URLs:**

Expand All @@ -17,7 +17,7 @@ This is a RESTful Web Service proof-of-concept built with **Spring Boot 4** targ
- **Java**: JDK 25 (LTS) - use modern Java features where appropriate
- **Spring Boot**: 4.0.0 with modular starter dependencies (WebMVC, Data JPA, Validation, Cache, Actuator)
- **Build Tool**: Maven 3.9+ (use `./mvnw` wrapper, NOT system Maven)
- **Database**: H2 in-memory database (runtime scope)
- **Database**: SQLite (runtime) with Xerial JDBC driver; H2 in-memory (test scope only)

### Key Dependencies

Expand All @@ -26,6 +26,8 @@ This is a RESTful Web Service proof-of-concept built with **Spring Boot 4** targ
- **SpringDoc OpenAPI** 2.8.14: API documentation (Swagger UI)
- **JaCoCo** 0.8.14: Code coverage reporting
- **AssertJ** 3.27.6: Fluent test assertions
- **SQLite JDBC** 3.47.1.0: SQLite database driver (Xerial)
- **Hibernate Community Dialects**: Provides `SQLiteDialect` for JPA/Hibernate

### Testing

Expand All @@ -36,8 +38,8 @@ This is a RESTful Web Service proof-of-concept built with **Spring Boot 4** targ

### DevOps & CI/CD

- **Docker**: Multi-stage build with Eclipse Temurin Alpine images
- **Docker Compose**: Local containerized deployment
- **Docker**: Multi-stage build with Eclipse Temurin Alpine images and pre-seeded SQLite database
- **Docker Compose**: Local containerized deployment with persistent storage volume
- **GitHub Actions**: CI pipeline with coverage reporting (Codecov, Codacy)

## Project Structure
Expand All @@ -54,7 +56,7 @@ src/main/java/ar/com/nanotaboada/java/samples/spring/boot/
└── models/ # Domain entities & DTOs
├── Book.java # JPA entity
├── BookDTO.java # Data Transfer Object with validation
└── BooksDataInitializer.java # Seed data
└── UnixTimestampConverter.java # JPA converter for LocalDate ↔ Unix timestamp

src/test/java/.../test/
├── controllers/ # Controller tests (@WebMvcTest)
Expand All @@ -64,12 +66,18 @@ src/test/java/.../test/
└── BookDTOFakes.java # Test data factory for BookDTO

src/main/resources/
├── application.properties # Application configuration
├── application.properties # Application configuration (SQLite)
└── logback-spring.xml # Logging configuration

src/test/resources/
└── application.properties # Test configuration (H2 in-memory)

scripts/
├── entrypoint.sh # Docker container entrypoint
├── entrypoint.sh # Docker entrypoint (copies seed DB on first run)
└── healthcheck.sh # Docker health check using Actuator

storage/
└── books-sqlite3.db # Pre-seeded SQLite database with sample books
```

**Package Naming Convention**: `ar.com.nanotaboada.java.samples.spring.boot.<layer>`
Expand Down Expand Up @@ -164,6 +172,15 @@ docker compose logs -f
- `9000`: Main API server
- `9001`: Actuator management endpoints

**Persistent Storage**:

The Docker container uses a "hold" pattern for the pre-seeded SQLite database:

1. Build stage copies `storage/books-sqlite3.db` to `/app/hold/` in the image
2. On first container run, `entrypoint.sh` copies the database to `/storage/` volume
3. Subsequent runs use the existing database from the volume
4. To reset: `docker compose down -v` removes volumes, next `up` restores seed data

## Common Tasks & Patterns

### Adding a New REST Endpoint
Expand Down Expand Up @@ -203,15 +220,18 @@ docker compose logs -f
### Build Failures

- **Lombok not working**: Ensure annotation processor is enabled in IDE and `maven-compiler-plugin` includes Lombok path
- **Tests failing**: Check if H2 database is properly initialized; review `BooksDataInitializer.seed()`
- **Tests failing**: Tests use H2 in-memory database via `src/test/resources/application.properties`
- **Port already in use**: Change `server.port` in `application.properties` or kill process using ports 9000/9001
- **JAVA_HOME not set**: Run `export JAVA_HOME=$(/usr/libexec/java_home -v 25)` on macOS or set to JDK 25 path on other systems
- **CacheManager errors in tests**: Add `@AutoConfigureCache` annotation to slice tests (`@WebMvcTest`, `@DataJpaTest`)
- **SQLite file not found**: Ensure `storage/books-sqlite3.db` exists for local development

### Docker Issues

- **Container health check failing**: Verify Actuator is accessible at `http://localhost:9001/actuator/health`
- **Build context too large**: Ensure `.dockerignore` excludes `target/` and `.git/`
- **Database not persisting**: Check that `java-samples-spring-boot_storage` volume exists (`docker volume ls`)
- **Stale seed data**: Run `docker compose down -v` to remove volumes and restore fresh seed data on next `up`

### Common Pitfalls

Expand All @@ -221,6 +241,13 @@ docker compose logs -f
- **Repository interfaces**: Custom query methods may not show in coverage (JaCoCo limitation)
- **Spring Boot 4.0 modular packages**: Test annotations like `@WebMvcTest`, `@DataJpaTest`, and `@AutoConfigureCache` are now in modular packages (e.g., `org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest`)

### SQLite Configuration Notes

- **Date storage**: LocalDate fields are stored as Unix timestamps (INTEGER) for robustness - no parsing issues
- **Converter**: `UnixTimestampConverter` handles LocalDate ↔ epoch seconds conversion via JPA `@Convert`
- **DDL auto**: Use `ddl-auto=none` since the database is pre-seeded (SQLite has limited ALTER TABLE support)
- **Tests use H2**: The converter works seamlessly with both H2 and SQLite databases

## CI/CD Pipeline

### GitHub Actions Workflow (`.github/workflows/maven.yml`)
Expand Down
14 changes: 11 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,17 @@ COPY --chmod=555 assets/ ./assets/
COPY --chmod=555 scripts/entrypoint.sh ./entrypoint.sh
COPY --chmod=555 scripts/healthcheck.sh ./healthcheck.sh

# Add system user
RUN addgroup -S spring && \
adduser -S -G spring spring
# The 'hold' is our storage compartment within the image. Here, we copy a
# pre-seeded SQLite database file, which Compose will mount as a persistent
# 'storage' volume when the container starts up.
COPY --chmod=555 storage/ ./hold/

# Install SQLite runtime libs, add non-root user and prepare volume mount point
RUN apk add --no-cache sqlite-libs && \
addgroup -S spring && \
adduser -S -G spring spring && \
mkdir -p /storage && \
chown -R spring:spring /storage

USER spring

Expand Down
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ A proof-of-concept RESTful Web Service built with **Spring Boot 4** targeting **
The service showcases:

- Multi-layer architecture (Controllers → Services → Repositories)
- In-memory H2 database with JPA/Hibernate
- SQLite database with JPA/Hibernate (H2 for tests)
- Spring Cache abstraction for performance optimization
- Comprehensive test coverage with JUnit 5, Mockito, and AssertJ
- OpenAPI 3.0 documentation with Swagger UI
- Production-ready monitoring with Spring Boot Actuator
- Containerized deployment with Docker
- Containerized deployment with Docker and persistent storage

## Features

Expand All @@ -47,7 +47,7 @@ The service showcases:
- ✅ **API Documentation** - Interactive Swagger UI powered by SpringDoc OpenAPI
- ✅ **Health Monitoring** - Spring Boot Actuator endpoints
- ✅ **Test Coverage** - JaCoCo reports with Codecov/Codacy integration
- ✅ **Docker Support** - Multi-stage builds with Eclipse Temurin Alpine images
- ✅ **Docker Support** - Multi-stage builds with pre-seeded SQLite database
- ✅ **CI/CD Ready** - GitHub Actions with automated testing and container builds

## Architecture
Expand Down Expand Up @@ -130,6 +130,17 @@ docker compose down
- `9000` - Main API server
- `9001` - Actuator management endpoints

**Persistent Storage:**

The Docker container uses a pre-seeded SQLite database with sample book data. On first run, the database is copied from the image to a named volume (`java-samples-spring-boot_storage`) ensuring data persistence across container restarts.

To reset the database to its initial state:

```bash
docker compose down -v # Remove volumes
docker compose up # Fresh start with seed data
```

## API Reference

The Books API provides standard CRUD operations:
Expand Down Expand Up @@ -180,6 +191,7 @@ open target/site/jacoco/index.html
**Test Structure:**

- **Unit Tests** - `@WebMvcTest`, `@DataJpaTest` for isolated layer testing (with `@AutoConfigureCache` for caching support)
- **Test Database** - H2 in-memory database for fast, isolated test execution
- **Mocking** - Mockito with `@MockitoBean` for dependency mocking
- **Assertions** - AssertJ fluent assertions
- **Naming Convention** - BDD style: `given<Condition>_when<Action>_then<Expected>`
Expand All @@ -190,6 +202,13 @@ open target/site/jacoco/index.html
- Services: 100%
- Repositories: Custom query methods (interfaces excluded by JaCoCo design)

**SQLite Configuration Notes:**

- Dates are stored as Unix timestamps (INTEGER) for robustness - no date format parsing issues
- A JPA `AttributeConverter` handles LocalDate ↔ epoch seconds conversion transparently (UTC-based)
- Use `ddl-auto=none` since the database is pre-seeded (SQLite has limited ALTER TABLE support)
- Tests use H2 in-memory database - the converter works seamlessly with both databases

## Documentation

- **API Documentation**: Swagger UI at `http://localhost:9000/swagger/index.html`
Expand Down
7 changes: 7 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ services:
ports:
- "9000:9000"
- "9001:9001"
volumes:
- storage:/storage/
environment:
- SPRING_PROFILES_ACTIVE=production
- STORAGE_PATH=/storage/books-sqlite3.db
restart: unless-stopped

volumes:
storage:
name: java-samples-spring-boot_storage
30 changes: 26 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,18 +146,40 @@
<artifactId>spring-boot-starter-data-jpa-test</artifactId>
<scope>test</scope>
</dependency>
<!-- SQLite JDBC Driver ============================================ -->
<!--
SQLite JDBC is a library for accessing SQLite databases through
the JDBC API. It provides a lightweight, file-based database that
persists data across application restarts.
https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc
-->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.51.0.0</version>
<scope>runtime</scope>
</dependency>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<!-- Hibernate Community Dialects ==================================== -->
<!--
Provides SQLite dialect for Hibernate ORM. Required for JPA/Hibernate
to correctly generate SQL statements for SQLite databases.
https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-community-dialects
-->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
</dependency>
<!-- H2 Database Engine ============================================ -->
<!--
Provides a fast in-memory database that supports JDBC API and R2DBC
access, with a small (2mb) footprint. Supports embedded and server
modes as well as a browser based console application.
Provides a fast in-memory database for testing. Used only in test
scope for fast unit and integration test execution.
https://mvnrepository.com/artifact/com.h2database/h2
-->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.4.240</version>
<scope>runtime</scope>
<scope>test</scope>
</dependency>
<!-- =============================================================== -->
<!-- Operations -->
Expand Down
31 changes: 26 additions & 5 deletions scripts/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
#!/bin/sh
set -e

echo "✔ Starting Spring Boot container..."
echo "✔ Executing entrypoint script..."

echo "✔ Server Port: ${SERVER_PORT:-9000}"
echo "✔ Management Port: ${MANAGEMENT_PORT:-9001}"
echo "✔ Active Profile(s): ${SPRING_PROFILES_ACTIVE:-default}"
IMAGE_STORAGE_PATH="/app/hold/books-sqlite3.db"
VOLUME_STORAGE_PATH="/storage/books-sqlite3.db"

echo "🚀 Launching Spring Boot app..."
echo "✔ Starting container..."

if [ ! -f "$VOLUME_STORAGE_PATH" ]; then
echo "⚠️ No existing database file found in volume."
if [ -f "$IMAGE_STORAGE_PATH" ]; then
echo "Copying database file to writable volume..."
if cp "$IMAGE_STORAGE_PATH" "$VOLUME_STORAGE_PATH"; then
echo "✔ Database initialized at $VOLUME_STORAGE_PATH"
else
echo "❌ Failed to copy database from $IMAGE_STORAGE_PATH to $VOLUME_STORAGE_PATH"
echo " Check file permissions and available disk space."
exit 1
fi
else
echo "⚠️ Database file missing at $IMAGE_STORAGE_PATH"
exit 1
fi
else
echo "✔ Existing database file found. Skipping seed copy."
fi

echo "✔ Ready!"
echo "🚀 Launching app..."
exec "$@"
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
package ar.com.nanotaboada.java.samples.spring.boot;

import java.util.List;

import org.modelmapper.ModelMapper;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;

import ar.com.nanotaboada.java.samples.spring.boot.models.Book;
import ar.com.nanotaboada.java.samples.spring.boot.models.BooksDataInitializer;
import ar.com.nanotaboada.java.samples.spring.boot.repositories.BooksRepository;

/**
* A configuration class that declares one or more Bean methods and also
* triggers auto-configuration and component scanning.
Expand All @@ -21,27 +14,11 @@
@EnableCaching
public class Application {

private final BooksRepository repository;

public Application(BooksRepository repository) {
this.repository = repository;
}

@Bean
ModelMapper modelMapper() {
return new ModelMapper();
}

@Bean
CommandLineRunner seed() {
return args -> {
List<Book> books = BooksDataInitializer.seed();
if (books != null) {
repository.saveAll(books);
}
};
}

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

import java.time.LocalDate;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;

import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;

@Entity
@Table(name = "books")
Expand All @@ -28,11 +28,21 @@ public class Book {
private String subtitle;
private String author;
private String publisher;
/**
* Stored as Unix timestamp (INTEGER) in SQLite for robustness.
* The converter handles LocalDate ↔ epoch seconds conversion.
*/
@JsonDeserialize(using = LocalDateDeserializer.class)
@JsonSerialize(using = LocalDateSerializer.class)
@Convert(converter = UnixTimestampConverter.class)
private LocalDate published;
private int pages;
@Lob
/**
* Maximum length set to 8192 (8^4 = 2^13) characters.
* This power-of-two value provides ample space for book descriptions
* while remaining compatible with both H2 (VARCHAR) and SQLite (TEXT).
*/
@Column(length = 8192)
private String description;
private String website;
}
Loading