diff --git a/README.md b/README.md index 12d2b84..4498689 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,26 @@ Jazzy is a lightweight web framework for Java. It provides a minimal and easy-to-understand API for developing fast web applications with a structure inspired by Laravel and Spring Boot. -## 🚀 Latest Updates (v0.2.0) +## 🚀 Latest Updates (v0.3.0) -**NEW: Enterprise-Level Dependency Injection System!** +**NEW: Database Integration & ORM System!** -Jazzy Framework 0.2 introduces a comprehensive Spring-like dependency injection system with zero configuration: +Jazzy Framework 0.3 introduces comprehensive database integration with Spring Data JPA-like features: -- 🔧 **Zero Configuration DI**: Automatic component discovery -- 📦 **Spring-like Annotations**: @Component, @Named, @Primary, @PostConstruct, @PreDestroy -- 🔌 **Constructor Injection**: Clean, testable dependency injection -- ⚖️ **Multiple Implementations**: Handle conflicts with @Named and @Primary -- 🔄 **Lifecycle Management**: Proper initialization and cleanup -- 📊 **Scope Management**: @Singleton and @Prototype support -- 🔗 **Framework Integration**: Seamless integration with routing and controllers +- 🗄️ **Hibernate Integration**: Full JPA/Hibernate support with automatic configuration +- 🔍 **Spring Data JPA-like Repositories**: Automatic query generation from method names +- 📝 **Custom Query Support**: @Query annotation for HQL/JPQL and native SQL +- 🔄 **Transaction Management**: Automatic transaction handling +- 🏗️ **Entity Management**: Automatic entity discovery and configuration +- 📊 **Connection Pooling**: HikariCP integration for production-ready performance +- 🎯 **Method Name Parsing**: `findByEmail`, `countByActive`, `existsByName` etc. +- ⚡ **Performance Optimized**: Database-level filtering instead of memory operations ## Version History | Version | Release Date | Key Features | |---------|-------------|--------------| +| **0.3.0** | 2025 | 🆕 **Database Integration** - Hibernate/JPA, Spring Data JPA-like repositories, automatic query generation, transaction management | | **0.2.0** | 2025 | 🆕 **Dependency Injection System**, Spring-like annotations, automatic component discovery, lifecycle management | | **0.1.0** | 2025 | Core framework with routing, request/response handling, JSON utilities, validation system, metrics | @@ -27,7 +29,7 @@ Jazzy Framework 0.2 introduces a comprehensive Spring-like dependency injection | Planned Version | Features | |----------------|----------| -| **0.3.0** | 🗄️ **Database Integration** - jOOQ integration, connection pooling, transaction management | +| **0.4.0** | 🔐 **Security & Authentication** - JWT support, role-based access control, security filters | ## Features @@ -48,111 +50,166 @@ Jazzy Framework 0.2 introduces a comprehensive Spring-like dependency injection - **Scope Management**: @Singleton (default) and @Prototype scopes - **Framework Integration**: DI works seamlessly with controllers and routing +### Database Integration (v0.3+) +- **Hibernate/JPA Integration**: Full ORM support with automatic configuration +- **Spring Data JPA-like Repositories**: Familiar repository pattern with automatic implementation +- **Method Name Parsing**: Automatic query generation from method names +- **Custom Queries**: @Query annotation for HQL/JPQL and native SQL queries +- **Transaction Management**: Automatic transaction handling with proper rollback +- **Entity Discovery**: Automatic entity scanning and configuration +- **Connection Pooling**: HikariCP for production-ready database connections + ## Quick Start -### Basic Application (v0.1 style) +### Database Application (v0.3 style) ```java -// App.java -package examples.basic; - -import jazzyframework.core.Config; -import jazzyframework.core.Server; -import jazzyframework.routing.Router; - -public class App -{ - public static void main( String[] args ) - { - Config config = new Config(); - config.setEnableMetrics(true); // "/metrics" endpoint is automatically added - config.setServerPort(8088); - - Router router = new Router(); - - // User routes - router.GET("/users/{id}", "getUserById", UserController.class); - router.GET("/users", "getAllUsers", UserController.class); - router.POST("/users", "createUser", UserController.class); - router.PUT("/users/{id}", "updateUser", UserController.class); - router.DELETE("/users/{id}", "deleteUser", UserController.class); - - // Start the server - Server server = new Server(router, config); - server.start(config.getServerPort()); - } +// Entity +@Entity +@Table(name = "users") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true) + private String email; + + private String name; + private String password; + private boolean active = true; + + // getters and setters... } -``` - -### With Dependency Injection (v0.2 style) -```java -// Repository Component -@Component -public class UserRepository { - private final List users = new ArrayList<>(); +// Repository with automatic query generation +public interface UserRepository extends BaseRepository { + // Automatic query: SELECT u FROM User u WHERE u.email = :email + Optional findByEmail(String email); - @PostConstruct - public void init() { - System.out.println("UserRepository initialized"); - } + // Automatic query: SELECT u FROM User u WHERE u.active = :active + List findByActive(boolean active); - public List findAll() { - return new ArrayList<>(users); - } + // Automatic query: SELECT COUNT(u) FROM User u WHERE u.active = :active + long countByActive(boolean active); + + // Custom query with @Query annotation + @Query("SELECT u FROM User u WHERE u.email = :email AND u.active = true") + Optional findActiveUserByEmail(String email); + + // Native SQL query + @Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true) + Optional findByEmailNative(String email); + + // Update query + @Query("UPDATE User u SET u.active = :active WHERE u.email = :email") + @Modifying + int updateUserActiveStatus(String email, boolean active); } -// Service Component +// Service @Component public class UserService { - private final UserRepository repository; + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public User createUser(String name, String email, String password) { + // Check if user already exists + if (userRepository.findByEmail(email).isPresent()) { + throw new IllegalArgumentException("User with email " + email + " already exists"); + } + + User user = new User(); + user.setName(name); + user.setEmail(email); + user.setPassword(password); + + return userRepository.save(user); + } - // Constructor injection - DI container automatically injects UserRepository - public UserService(UserRepository repository) { - this.repository = repository; + public Optional findByEmail(String email) { + return userRepository.findByEmail(email); } - public List getAllUsers() { - return repository.findAll(); + public List findAllUsers() { + return userRepository.findAll(); } } -// Controller Component +// Controller @Component public class UserController { private final UserService userService; - // Constructor injection - DI container automatically injects UserService public UserController(UserService userService) { this.userService = userService; } - - public Response getUsers(Request request) { - return Response.json(userService.getAllUsers()); + + public Response createUser(Request request) { + User user = request.toObject(User.class); + User createdUser = userService.createUser(user.getName(), user.getEmail(), user.getPassword()); + return Response.json(JSON.of("result", createdUser)); + } + + public Response getUserByEmail(Request request) { + String email = request.query("email"); + Optional user = userService.findByEmail(email); + + if (user.isPresent()) { + return Response.json(Map.of("user", user.get())); + } else { + return Response.json(Map.of("error", "User not found")).status(404); + } } } -// Application - DI works automatically! +// Main class public class App { public static void main(String[] args) { Config config = new Config(); + config.setEnableMetrics(true); + config.setServerPort(8080); + Router router = new Router(); - // Define routes - controllers will be created with DI - router.GET("/users", "getUsers", UserController.class); + // User routes + router.GET("/users", "getAllUsers", UserController.class); + router.GET("/users/search", "getUserByEmail", UserController.class); + router.POST("/users", "createUser", UserController.class); - // DI is automatically enabled and configured Server server = new Server(router, config); - server.start(8080); + server.start(config.getServerPort()); } } ``` -That's it! The DI container automatically: -- Discovers all `@Component` classes -- Resolves dependencies between them -- Creates instances with proper injection -- Manages lifecycle callbacks +### Configuration (application.properties) + +```properties +# Database Configuration +jazzy.datasource.url=jdbc:h2:mem:testdb +jazzy.datasource.username=sa +jazzy.datasource.password= +jazzy.datasource.driver-class-name=org.h2.Driver + +# JPA/Hibernate Configuration +jazzy.jpa.hibernate.ddl-auto=create-drop +jazzy.jpa.show-sql=true +jazzy.jpa.hibernate.dialect=org.hibernate.dialect.H2Dialect + +# H2 Console (for development) +jazzy.h2.console.enabled=true +jazzy.h2.console.path=/h2-console +``` + +That's it! The framework automatically: +- Discovers entities and repositories +- Creates repository implementations with query parsing +- Manages database connections and transactions +- Provides Spring Data JPA-like functionality ## Documentation @@ -174,6 +231,11 @@ Complete documentation for Jazzy Framework is available on our GitHub Pages site - [Dependency Injection Guide](https://canermastan.github.io/jazzy-framework/dependency-injection) - [DI Examples](https://canermastan.github.io/jazzy-framework/di-examples) +**Database Integration (v0.3+):** +- [Database Integration Guide](https://canermastan.github.io/jazzy-framework/database-integration) +- [Repository Pattern](https://canermastan.github.io/jazzy-framework/repositories) +- [Query Methods](https://canermastan.github.io/jazzy-framework/query-methods) + ## Development Jazzy is developed with Maven. After cloning the project, you can use the following commands: @@ -190,6 +252,9 @@ mvn exec:java -Dexec.mainClass="examples.basic.App" # Run the DI example application (v0.2+) mvn exec:java -Dexec.mainClass="examples.di.App" + +# Run the database example application (v0.3+) +mvn exec:java -Dexec.mainClass="examples.database.DatabaseExampleApp" ``` ## Project Structure @@ -199,6 +264,7 @@ mvn exec:java -Dexec.mainClass="examples.di.App" - `RequestHandler.java`: HTTP request processor with DI integration - `Config.java`: Configuration management - `Metrics.java`: Performance metrics + - `PropertyLoader.java`: Configuration property management (v0.3+) - `routing/`: Routing system - `Router.java`: Route management with DI container support - `Route.java`: Route data structure @@ -213,11 +279,21 @@ mvn exec:java -Dexec.mainClass="examples.di.App" - `ComponentScanner.java`: Automatic component scanning - `BeanDefinition.java`: Bean metadata and lifecycle management - `annotations/`: DI annotations (@Component, @Named, @Primary, etc.) +- `data/`: Database integration system (v0.3+) + - `BaseRepository.java`: Base repository interface + - `BaseRepositoryImpl.java`: Default repository implementation + - `RepositoryFactory.java`: Repository proxy creation + - `QueryMethodParser.java`: Method name to query parsing + - `HibernateConfig.java`: Hibernate/JPA configuration + - `EntityScanner.java`: Automatic entity discovery + - `RepositoryScanner.java`: Repository interface scanning + - `annotations/`: Database annotations (@Query, @Modifying, @QueryHint) - `controllers/`: System controllers - `MetricsController.java`: Metrics reporting - `examples/`: Example applications - - `basic/`: A simple web API example (v0.1 style) - - `di/`: Dependency injection example (v0.2 style) + - `basic/`: Basic framework usage examples + - `di/`: Dependency injection examples (v0.2+) + - `database/`: Database integration examples (v0.3+) ## Tests diff --git a/docs-site/docs/database-integration.md b/docs-site/docs/database-integration.md new file mode 100644 index 0000000..34fc5a2 --- /dev/null +++ b/docs-site/docs/database-integration.md @@ -0,0 +1,619 @@ +# Database Integration + +Jazzy Framework 0.3 introduces comprehensive database integration with Spring Data JPA-like functionality. The framework provides automatic entity discovery, repository pattern implementation, and zero-configuration database setup. + +## Overview + +The database integration system provides: + +- **Hibernate/JPA Integration**: Full ORM support with automatic configuration +- **Spring Data JPA-like Repositories**: Familiar repository pattern with automatic implementation +- **Method Name Parsing**: Automatic query generation from method names +- **Custom Query Support**: @Query annotation for HQL/JPQL and native SQL +- **Transaction Management**: Automatic transaction handling with proper rollback +- **Entity Discovery**: Automatic entity scanning and configuration +- **Connection Pooling**: HikariCP for production-ready database connections +- **Multiple Database Support**: H2, PostgreSQL, MySQL, Oracle support + +## Quick Start + +### 1. Configuration + +Create `application.properties` in your `src/main/resources` folder: + +```properties +# Database Configuration +jazzy.datasource.url=jdbc:h2:mem:testdb +jazzy.datasource.username=sa +jazzy.datasource.password= +jazzy.datasource.driver-class-name=org.h2.Driver + +# JPA/Hibernate Configuration +jazzy.jpa.hibernate.ddl-auto=create-drop +jazzy.jpa.show-sql=true +jazzy.jpa.hibernate.dialect=org.hibernate.dialect.H2Dialect + +# H2 Console (for development) +jazzy.h2.console.enabled=true +jazzy.h2.console.path=/h2-console +``` + +### 2. Create an Entity + +```java +package com.example.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "users") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String email; + + @Column(nullable = false) + private String name; + + private String password; + private boolean active = true; + + // Constructors + public User() {} + + public User(String name, String email, String password) { + this.name = name; + this.email = email; + this.password = password; + } + + // Getters and setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + + public boolean isActive() { return active; } + public void setActive(boolean active) { this.active = active; } +} +``` + +### 3. Create a Repository + +```java +package com.example.repository; + +import com.example.entity.User; +import jazzyframework.data.BaseRepository; +import jazzyframework.data.annotations.Query; +import jazzyframework.data.annotations.Modifying; + +import java.util.List; +import java.util.Optional; + +public interface UserRepository extends BaseRepository { + + // Automatic query generation from method names + Optional findByEmail(String email); + List findByActive(boolean active); + List findByNameContaining(String name); + long countByActive(boolean active); + boolean existsByEmail(String email); + + // Custom HQL/JPQL queries + @Query("SELECT u FROM User u WHERE u.email = :email AND u.active = true") + Optional findActiveUserByEmail(String email); + + @Query("SELECT u FROM User u WHERE u.name LIKE %:name% ORDER BY u.name") + List searchByName(String name); + + // Native SQL queries + @Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true) + Optional findByEmailNative(String email); + + // Update operations + @Query("UPDATE User u SET u.active = :active WHERE u.email = :email") + @Modifying + int updateUserActiveStatus(String email, boolean active); + + @Query("DELETE FROM User u WHERE u.active = false") + @Modifying + int deleteInactiveUsers(); +} +``` + +### 4. Create a Service + +```java +package com.example.service; + +import com.example.entity.User; +import com.example.repository.UserRepository; +import jazzyframework.di.annotations.Component; + +import java.util.List; +import java.util.Optional; + +@Component +public class UserService { + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public User createUser(String name, String email, String password) { + // Business logic validation + if (userRepository.existsByEmail(email)) { + throw new IllegalArgumentException("User with email " + email + " already exists"); + } + + User user = new User(name, email, password); + return userRepository.save(user); + } + + public Optional findByEmail(String email) { + return userRepository.findByEmail(email); + } + + public List findActiveUsers() { + return userRepository.findByActive(true); + } + + public List searchUsers(String name) { + return userRepository.findByNameContaining(name); + } + + public boolean deactivateUser(String email) { + int updated = userRepository.updateUserActiveStatus(email, false); + return updated > 0; + } + + public long getActiveUserCount() { + return userRepository.countByActive(true); + } +} +``` + +### 5. Use in Controller + +```java +package com.example.controller; + +import com.example.entity.User; +import com.example.service.UserService; +import jazzyframework.di.annotations.Component; +import jazzyframework.http.Request; +import jazzyframework.http.Response; +import jazzyframework.http.JSON; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Component +public class UserController { + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + public Response createUser(Request request) { + User user = request.toObject(User.class); + User createdUser = userService.createUser(user.getName(), user.getEmail(), user.getPassword()); + return Response.json(JSON.of("user", createdUser)); + } + + public Response getUserByEmail(Request request) { + String email = request.query("email"); + Optional user = userService.findByEmail(email); + + if (user.isPresent()) { + return Response.json(JSON.of("user", user.get())); + } else { + return Response.json(JSON.of("error", "User not found")).status(404); + } + } + + public Response getActiveUsers(Request request) { + List users = userService.findActiveUsers(); + return Response.json(JSON.of("users", users, "count", users.size())); + } + + public Response searchUsers(Request request) { + String name = request.query("name"); + List users = userService.searchUsers(name); + return Response.json(JSON.of("users", users)); + } +} +``` + +## Configuration Options + +### Database Configuration + +```properties +# Database Connection +jazzy.datasource.url=jdbc:h2:mem:testdb +jazzy.datasource.username=sa +jazzy.datasource.password= +jazzy.datasource.driver-class-name=org.h2.Driver + +# Connection Pool (HikariCP) +jazzy.datasource.hikari.maximum-pool-size=10 +jazzy.datasource.hikari.minimum-idle=5 +jazzy.datasource.hikari.connection-timeout=30000 +jazzy.datasource.hikari.idle-timeout=600000 +jazzy.datasource.hikari.max-lifetime=1800000 +``` + +### JPA/Hibernate Configuration + +```properties +# Schema Management +jazzy.jpa.hibernate.ddl-auto=create-drop # create, update, validate, none + +# SQL Logging +jazzy.jpa.show-sql=true +jazzy.jpa.hibernate.format_sql=true + +# Dialect (auto-detected if not specified) +jazzy.jpa.hibernate.dialect=org.hibernate.dialect.H2Dialect + +# Performance Settings +jazzy.jpa.properties.hibernate.jdbc.batch_size=20 +jazzy.jpa.properties.hibernate.order_inserts=true +jazzy.jpa.properties.hibernate.order_updates=true +``` + +### H2 Console Configuration + +```properties +# H2 Console (Development Only) +jazzy.h2.console.enabled=true +jazzy.h2.console.path=/h2-console +jazzy.h2.console.port=8082 +``` + +## Supported Databases + +### H2 (Development) + +```properties +jazzy.datasource.url=jdbc:h2:mem:testdb +jazzy.datasource.driver-class-name=org.h2.Driver +jazzy.jpa.hibernate.dialect=org.hibernate.dialect.H2Dialect +``` + +### MySQL (Production) + +```properties +jazzy.datasource.url=jdbc:mysql://localhost:3306/myapp +jazzy.datasource.username=myuser +jazzy.datasource.password=mypassword +jazzy.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +jazzy.jpa.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect +``` + +### PostgreSQL (Production) + +```properties +jazzy.datasource.url=jdbc:postgresql://localhost:5432/myapp +jazzy.datasource.username=myuser +jazzy.datasource.password=mypassword +jazzy.datasource.driver-class-name=org.postgresql.Driver +jazzy.jpa.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +``` + +## Entity Relationships + +### One-to-Many + +```java +@Entity +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List posts = new ArrayList<>(); + + // getters and setters +} + +@Entity +public class Post { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + // getters and setters +} +``` + +### Many-to-Many + +```java +@Entity +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToMany + @JoinTable( + name = "user_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id") + ) + private Set roles = new HashSet<>(); + + // getters and setters +} + +@Entity +public class Role { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToMany(mappedBy = "roles") + private Set users = new HashSet<>(); + + // getters and setters +} +``` + +## Transaction Management + +Transactions are automatically managed at the repository level. Each repository method runs in its own transaction: + +```java +// Automatic transaction management +public User createUser(String name, String email) { + User user = new User(name, email); + return userRepository.save(user); // Automatically wrapped in transaction +} + +// Multiple operations in same transaction +public void transferData() { + // For complex operations requiring multiple repository calls, + // consider implementing custom repository methods + userRepository.updateUserStatus(email, "ACTIVE"); + userRepository.updateLastLogin(email, new Date()); +} +``` + +## Performance Optimization + +### Connection Pooling + +HikariCP is automatically configured for optimal performance: + +```properties +# Optimize for your application +jazzy.datasource.hikari.maximum-pool-size=20 +jazzy.datasource.hikari.minimum-idle=5 +jazzy.datasource.hikari.connection-timeout=30000 +``` + +### Query Optimization + +```java +// Use specific queries instead of findAll() +List findByActive(boolean active); // Better than findAll() + filter + +// Use count queries for existence checks +boolean existsByEmail(String email); // Better than findByEmail().isPresent() + +// Use batch operations for multiple inserts +List saveAll(Iterable users); +``` + +### Lazy Loading + +```java +@Entity +public class User { + @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) // Lazy by default + private List posts; + + @ManyToOne(fetch = FetchType.EAGER) // Only when always needed + private Department department; +} +``` + +## Error Handling + +The framework provides automatic error handling for common database scenarios: + +```java +try { + User user = userService.createUser(name, email, password); + return Response.json(JSON.of("user", user)); +} catch (IllegalArgumentException e) { + // Business logic errors (e.g., duplicate email) + return Response.json(JSON.of("error", e.getMessage())).status(400); +} catch (Exception e) { + // Database errors are automatically handled by framework + return Response.json(JSON.of("error", "Internal server error")).status(500); +} +``` + +## Migration from Manual Data Handling + +If you're migrating from manual data handling to database integration: + +### Before (Manual) + +```java +@Component +public class UserService { + private final List users = new ArrayList<>(); + + public User createUser(String name, String email) { + User user = new User(name, email); + users.add(user); + return user; + } + + public Optional findByEmail(String email) { + return users.stream() + .filter(u -> u.getEmail().equals(email)) + .findFirst(); + } +} +``` + +### After (Database) + +```java +@Component +public class UserService { + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public User createUser(String name, String email) { + User user = new User(name, email); + return userRepository.save(user); // Automatically persisted + } + + public Optional findByEmail(String email) { + return userRepository.findByEmail(email); // Database query + } +} +``` + +## Best Practices + +### 1. Repository Design + +```java +// Good: Specific, focused repository +public interface UserRepository extends BaseRepository { + Optional findByEmail(String email); + List findByActive(boolean active); + long countByDepartment(String department); +} + +// Avoid: Generic, unfocused repository +public interface DataRepository extends BaseRepository { + // Too generic, hard to maintain +} +``` + +### 2. Service Layer + +```java +// Good: Business logic in service layer +@Component +public class UserService { + public User createUser(String name, String email) { + // Validation + if (userRepository.existsByEmail(email)) { + throw new IllegalArgumentException("Email already exists"); + } + + // Business logic + User user = new User(name, email); + user.setCreatedAt(new Date()); + + return userRepository.save(user); + } +} + +// Avoid: Business logic in controller +@Component +public class UserController { + public Response createUser(Request request) { + // Don't put business logic here + User user = request.toObject(User.class); + return Response.json(userRepository.save(user)); + } +} +``` + +### 3. Entity Design + +```java +// Good: Proper JPA annotations +@Entity +@Table(name = "users", indexes = { + @Index(name = "idx_user_email", columnList = "email"), + @Index(name = "idx_user_active", columnList = "active") +}) +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false, length = 255) + private String email; + + @Column(nullable = false, length = 100) + private String name; + + // Proper getters and setters +} +``` + +## Troubleshooting + +### Common Issues + +**1. "No repository found" error** +- Ensure your repository interface extends `BaseRepository` +- Check that your repository is in a package that gets scanned + +**2. "Entity not found" error** +- Verify your entity has `@Entity` annotation +- Check that entity is in a package that gets scanned + +**3. Database connection errors** +- Verify `application.properties` configuration +- Check database driver is in classpath +- Ensure database server is running (for external databases) + +**4. Query parsing errors** +- Check method name follows naming conventions +- Use `@Query` annotation for complex queries +- Verify parameter names match method parameters + +### Debug Mode + +Enable debug logging to troubleshoot issues: + +```properties +# Enable SQL logging +jazzy.jpa.show-sql=true +jazzy.jpa.hibernate.format_sql=true + +# Enable Hibernate logging +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +``` + +## Next Steps + +- [Repository Pattern](repositories.md) - Deep dive into repository interfaces +- [Query Methods](query-methods.md) - Method name parsing and custom queries +- [Database Examples](database-examples.md) - Complete working examples \ No newline at end of file diff --git a/docs-site/docs/getting-started.md b/docs-site/docs/getting-started.md index 6419c01..61cf932 100644 --- a/docs-site/docs/getting-started.md +++ b/docs-site/docs/getting-started.md @@ -4,7 +4,7 @@ This guide explains the steps needed to start working with Jazzy Framework. It c ## Requirements -- Java 8 or higher +- Java 17 or higher - Maven ## Installation @@ -15,7 +15,7 @@ Jazzy is designed to be used as a Maven project. You can start by adding the Jaz com.jazzyframework jazzy-framework - 0.1.0 + 0.3.0 ``` diff --git a/docs-site/docs/index.md b/docs-site/docs/index.md index cddad47..2d8c612 100644 --- a/docs-site/docs/index.md +++ b/docs-site/docs/index.md @@ -1,79 +1,266 @@ # Jazzy Framework -Jazzy is a lightweight, fast, and easy-to-use web framework developed in Java. Inspired by Laravel's elegant syntax, Jazzy brings modern, fluent APIs to the Java world with enterprise-level dependency injection capabilities. +Jazzy is a lightweight, fast, and easy-to-use web framework developed in Java. Inspired by Laravel's elegant syntax, Jazzy brings modern, fluent APIs to the Java world with enterprise-level dependency injection capabilities and comprehensive database integration. + +## 🚀 What's New in v0.3.0 + +**Major Database Integration Update!** + +- 🗄️ **Full Database Integration**: Hibernate/JPA support with zero configuration +- 🔍 **Spring Data JPA-like Repositories**: Automatic query generation from method names +- 📝 **Custom Query Support**: @Query annotation for HQL/JPQL and native SQL +- 🔄 **Transaction Management**: Automatic transaction handling with proper rollback +- 🏗️ **Entity Discovery**: Automatic entity scanning and configuration +- 📊 **Connection Pooling**: HikariCP for production-ready database connections ## Key Features - **Lightweight Architecture**: Provides core functionality with minimal dependencies - **Fluent API**: Easy and readable coding with Laravel-like fluent interfaces -- **Simple Routing**: Simple routing mechanism based on HTTP methods - **Request Processing**: Easily process request parameters, body, and headers - **Response Generation**: Create JSON, HTML, and other response types - **JSON Operations**: Easy JSON creation and processing capabilities -- **Validation**: Powerful validation system with Fluent API -- **🆕 Dependency Injection**: Spring-like DI system with zero configuration (v0.2+) -- **🆕 Automatic Component Discovery**: No manual setup required -- **🆕 Advanced Annotations**: @Component, @Named, @Primary, @PostConstruct, @PreDestroy -- **🆕 Lifecycle Management**: Proper initialization and cleanup -- **🆕 Multiple Scopes**: Singleton and Prototype bean management +- **Validation System**: Built-in request validation with custom rules +- **Dependency Injection**: Enterprise-level DI container with automatic component scanning +- **Database Integration**: Full ORM support with Spring Data JPA-like repositories +- **Entity Management**: Automatic entity discovery and configuration +- **Query Generation**: Automatic query generation from method names +- **Custom Queries**: Support for complex HQL/JPQL and native SQL queries +- **Transaction Management**: Automatic transaction handling +- **Connection Pooling**: Production-ready database connection management + +## Quick Start + +### 1. Add Dependency + +```xml + + com.jazzyframework + jazzy-framework + 0.3.0 + +``` + +### 2. Create Your First Application + +```java +import jazzyframework.core.Config; +import jazzyframework.core.Server; +import jazzyframework.routing.Router; +import jazzyframework.http.Request; +import jazzyframework.http.Response; +import jazzyframework.http.JSON; +import jazzyframework.di.annotations.Component; + +public class MyApp { + public static void main(String[] args) { + Config config = new Config(); + config.setEnableMetrics(true); // Enables "/metrics" endpoint automatically + config.setServerPort(8080); + + Router router = new Router(); + + // Controller-based routes with dependency injection + router.GET("/users", "getAllUsers", UserController.class); + router.GET("/users/{id}", "getUserById", UserController.class); + router.POST("/users", "createUser", UserController.class); + router.PUT("/users/{id}", "updateUser", UserController.class); + router.DELETE("/users/{id}", "deleteUser", UserController.class); + + Server server = new Server(router, config); + server.start(config.getServerPort()); + } +} + +// Example Controller with Dependency Injection +@Component +public class UserController { + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + public Response getAllUsers(Request request) { + List users = userService.findAllUsers(); + return Response.json(JSON.of( + "users", users, + "count", users.size() + )); + } + + public Response createUser(Request request) { + User user = request.toObject(User.class); + User createdUser = userService.createUser(user.getName(), user.getEmail()); + return Response.json(JSON.of("user", createdUser)).status(201); + } + + public Response getUserById(Request request) { + Long id = Long.parseLong(request.path("id")); + Optional user = userService.findById(id); + + if (user.isPresent()) { + return Response.json(JSON.of("user", user.get())); + } + return Response.json(JSON.of("error", "User not found")).status(404); + } + + public Response updateUser(Request request) { + Long id = Long.parseLong(request.path("id")); + User user = request.toObject(User.class); + User updatedUser = userService.updateUser(id, user); + return Response.json(JSON.of("user", updatedUser)); + } + + public Response deleteUser(Request request) { + Long id = Long.parseLong(request.path("id")); + userService.deleteUser(id); + return Response.json(JSON.of("message", "User deleted successfully")); + } +} + +### 3. Database Integration (NEW!) + +**Configure Database:** +```properties +# application.properties +jazzy.datasource.url=jdbc:h2:mem:testdb +jazzy.datasource.username=sa +jazzy.datasource.password= +jazzy.jpa.hibernate.ddl-auto=create-drop +``` + +**Create Entity:** +```java +@Entity +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true) + private String email; + private String name; + + // constructors, getters, setters... +} +``` + +**Create Repository:** +```java +public interface UserRepository extends BaseRepository { + Optional findByEmail(String email); + List findByActive(boolean active); + + @Query("SELECT u FROM User u WHERE u.name LIKE %:name%") + List searchByName(String name); +} +``` + +**Use in Service:** +```java +@Component +public class UserService { + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public User createUser(String name, String email) { + User user = new User(name, email); + return userRepository.save(user); + } +} +``` -## What's New in v0.2 +## Architecture Overview -Jazzy Framework 0.2 introduces a comprehensive **dependency injection system** that brings enterprise-level capabilities while maintaining the framework's simplicity: +Jazzy Framework follows a clean, modular architecture: -- **Zero Configuration DI**: Automatic component discovery with no XML or manual setup -- **Spring-like Annotations**: Familiar annotations for Spring developers -- **Constructor Injection**: Clean, testable dependency injection -- **Named Injection**: Handle multiple implementations with `@Named` -- **Primary Bean Selection**: Conflict resolution with `@Primary` -- **Lifecycle Management**: `@PostConstruct` and `@PreDestroy` callbacks -- **Scope Management**: `@Singleton` (default) and `@Prototype` scopes -- **Framework Integration**: DI works seamlessly with routing and controllers +``` +┌─────────────────────────────────────────────────────────────┐ +│ Jazzy Framework │ +├─────────────────────────────────────────────────────────────┤ +│ 🌐 HTTP Layer │ +│ ├── Routing & Request Handling │ +│ ├── Response Generation │ +│ └── Validation System │ +├─────────────────────────────────────────────────────────────┤ +│ 🔧 Dependency Injection │ +│ ├── Component Scanning │ +│ ├── Automatic Wiring │ +│ └── Lifecycle Management │ +├─────────────────────────────────────────────────────────────┤ +│ 🗄️ Database Integration (NEW in 0.3.0) │ +│ ├── Entity Management │ +│ ├── Repository Pattern │ +│ ├── Query Generation │ +│ └── Transaction Management │ +├─────────────────────────────────────────────────────────────┤ +│ 🛠️ Utilities │ +│ ├── JSON Processing │ +│ ├── Configuration Management │ +│ └── Logging & Metrics │ +└─────────────────────────────────────────────────────────────┘ +``` -## Inspiration +## Why Choose Jazzy? -Jazzy Framework is inspired by the elegant and user-friendly APIs of the Laravel PHP framework and Spring Boot's powerful dependency injection. Laravel's fluent method calls like `response()->json()` combined with Spring's annotation-driven DI form the core design philosophy of Jazzy. +### 🚀 **Developer Productivity** +- Minimal boilerplate code +- Intuitive, Laravel-inspired API +- Automatic dependency injection +- Zero-configuration database setup -## Why Jazzy? +### 🏗️ **Enterprise Ready** +- Production-ready dependency injection +- Comprehensive database integration +- Automatic transaction management +- Connection pooling with HikariCP -Jazzy provides a modern and clean API for Java web applications with enterprise-ready features. It's simple to use for both beginners and experienced developers and doesn't require detailed configuration. In particular: +### 🔧 **Modern Java Development** +- Clean, readable code patterns +- Type-safe repository interfaces +- Automatic query generation +- Support for modern Java features -- **Quick Start**: Create a working API in minutes with minimal setup -- **Readable Code**: Write code that is easy to read and maintain thanks to fluent APIs -- **Enterprise Ready**: Dependency injection for scalable, maintainable applications -- **Zero Configuration**: DI works out of the box with automatic component discovery -- **Lightweight Structure**: Avoid the burden of complex frameworks -- **Easy Learning**: Familiar for those who know Laravel/Spring-like structures, intuitive for those who don't +### 📈 **Performance Focused** +- Lightweight core with minimal overhead +- Efficient database operations +- Optimized connection pooling +- Database-level query optimization -## Version +## Learning Path -Jazzy Framework is currently at version **0.2.0**. This major release introduces dependency injection capabilities while maintaining full backward compatibility with 0.1.x applications. +### 🌱 **Beginner** +1. [Getting Started](getting-started.md) - Set up your first Jazzy application +2. [Routing](routing.md) - Learn URL routing and HTTP methods +3. [Requests & Responses](requests.md) - Handle HTTP requests and responses -## Getting Started +### 🌿 **Intermediate** +4. [Dependency Injection](dependency-injection.md) - Master the DI container +5. [Database Integration](database-integration.md) - Set up database connectivity +6. [Repository Pattern](repositories.md) - Create data access layers -To start working with Jazzy, you can review the [Getting Started Guide](getting-started.md). +### 🌳 **Advanced** +7. [Query Methods](query-methods.md) - Advanced database querying +8. [Validation](validation.md) - Request validation and error handling +9. [Examples](examples.md) - Real-world application examples -## Contents +## Community & Support -### Core Framework -1. [Getting Started Guide](getting-started.md) - Installation and basic application -2. [Routing](routing.md) - Route definition and usage -3. [HTTP Requests](requests.md) - Request class and request processing -4. [HTTP Responses](responses.md) - Response class and response generation -5. [JSON Operations](json.md) - Creating and processing JSON data -6. [ResponseFactory](response_factory.md) - Class that simplifies response creation -7. [Validation](validation.md) - Request validation and error handling -8. [Examples](examples.md) - Complete application examples +- **GitHub**: [JazzyFramework Repository](https://github.com/canermastan/jazzy-framework) +- **Documentation**: Complete guides and API reference +- **Examples**: Working code samples and tutorials +- **Issues**: Bug reports and feature requests -### Dependency Injection (v0.2+) -9. [Dependency Injection](dependency-injection.md) - Comprehensive DI guide -10. [DI Examples](di-examples.md) - Complete DI examples and patterns +## Version History -## Migration from 0.1 to 0.2 +- **v0.3.0** (Current) - Database Integration & ORM Support +- **v0.2.0** - Dependency Injection & Component System +- **v0.1.0** - Core HTTP Framework & Routing -Upgrading to 0.2 is **seamless** - all existing 0.1 code continues to work without modification. The dependency injection system is purely additive: +--- -- Existing controllers and services work as before -- Add `@Component` annotations to enable DI for specific classes -- Gradually migrate to DI-based architecture -- Mix manual instantiation with DI in the same application \ No newline at end of file +Ready to build amazing applications with Jazzy? Start with our [Getting Started Guide](getting-started.md)! \ No newline at end of file diff --git a/docs-site/docs/query-methods.md b/docs-site/docs/query-methods.md new file mode 100644 index 0000000..c785866 --- /dev/null +++ b/docs-site/docs/query-methods.md @@ -0,0 +1,659 @@ +# Query Methods + +Jazzy Framework provides powerful query method capabilities that automatically generate database queries from method names, similar to Spring Data JPA. This feature eliminates the need to write SQL for common query patterns while maintaining type safety and readability. + +## Overview + +Query methods in Jazzy support: + +- **Method Name Parsing**: Automatic query generation from method names +- **Custom Queries**: @Query annotation for complex HQL/JPQL and native SQL +- **Parameter Binding**: Automatic parameter mapping and type conversion +- **Return Type Flexibility**: Support for Optional, List, primitives, and custom types +- **Performance Optimization**: Database-level filtering instead of memory operations + +## Method Name Query Generation + +### Basic Syntax + +Query methods follow the pattern: `By` + +```java +// Basic pattern examples +Optional findByEmail(String email); // SELECT u FROM User u WHERE u.email = ?1 +List findByActive(boolean active); // SELECT u FROM User u WHERE u.active = ?1 +long countByActive(boolean active); // SELECT COUNT(u) FROM User u WHERE u.active = ?1 +boolean existsByEmail(String email); // SELECT COUNT(u) FROM User u WHERE u.email = ?1 > 0 +void deleteByActive(boolean active); // DELETE FROM User u WHERE u.active = ?1 +``` + +### Supported Operations + +| Operation | Description | Example | Generated Query | +|-----------|-------------|---------|-----------------| +| `find` | Retrieve entities | `findByEmail` | `SELECT u FROM User u WHERE u.email = ?1` | +| `count` | Count entities | `countByActive` | `SELECT COUNT(u) FROM User u WHERE u.active = ?1` | +| `exists` | Check existence | `existsByEmail` | `SELECT COUNT(u) FROM User u WHERE u.email = ?1 > 0` | +| `delete` | Delete entities | `deleteByActive` | `DELETE FROM User u WHERE u.active = ?1` | + +### Property Conditions + +#### Equality + +```java +// Simple equality +Optional findByEmail(String email); +List findByName(String name); +List findByActive(boolean active); + +// Generated queries +// SELECT u FROM User u WHERE u.email = :email +// SELECT u FROM User u WHERE u.name = :name +// SELECT u FROM User u WHERE u.active = :active +``` + +#### Comparison Operations + +```java +// Numeric comparisons +List findByAgeGreaterThan(int age); +List findByAgeLessThan(int age); +List findByAgeGreaterThanEqual(int age); +List findByAgeLessThanEqual(int age); +List findByAgeBetween(int minAge, int maxAge); + +// Date comparisons +List findByCreatedDateAfter(Date date); +List findByCreatedDateBefore(Date date); +List findByCreatedDateBetween(Date start, Date end); + +// Generated queries +// SELECT u FROM User u WHERE u.age > :age +// SELECT u FROM User u WHERE u.age < :age +// SELECT u FROM User u WHERE u.age >= :age +// SELECT u FROM User u WHERE u.age <= :age +// SELECT u FROM User u WHERE u.age BETWEEN :minAge AND :maxAge +``` + +#### String Operations + +```java +// String matching +List findByNameLike(String pattern); +List findByNameContaining(String substring); +List findByNameStartingWith(String prefix); +List findByNameEndingWith(String suffix); +List findByNameIgnoreCase(String name); + +// Generated queries +// SELECT u FROM User u WHERE u.name LIKE :pattern +// SELECT u FROM User u WHERE u.name LIKE %:substring% +// SELECT u FROM User u WHERE u.name LIKE :prefix% +// SELECT u FROM User u WHERE u.name LIKE %:suffix +// SELECT u FROM User u WHERE UPPER(u.name) = UPPER(:name) +``` + +#### Null Checks + +```java +// Null/Not null checks +List findByLastLoginIsNull(); +List findByLastLoginIsNotNull(); +List findByEmailNull(); +List findByEmailNotNull(); + +// Generated queries +// SELECT u FROM User u WHERE u.lastLogin IS NULL +// SELECT u FROM User u WHERE u.lastLogin IS NOT NULL +// SELECT u FROM User u WHERE u.email IS NULL +// SELECT u FROM User u WHERE u.email IS NOT NULL +``` + +#### Boolean Operations + +```java +// Boolean values +List findByActiveTrue(); +List findByActiveFalse(); + +// Generated queries +// SELECT u FROM User u WHERE u.active = true +// SELECT u FROM User u WHERE u.active = false +``` + +#### Collection Operations + +```java +// In/Not in collections +List findByAgeIn(Collection ages); +List findByAgeNotIn(Collection ages); +List findByStatusIn(List statuses); + +// Generated queries +// SELECT u FROM User u WHERE u.age IN :ages +// SELECT u FROM User u WHERE u.age NOT IN :ages +// SELECT u FROM User u WHERE u.status IN :statuses +``` + +### Logical Operators + +#### AND Operations + +```java +// Multiple conditions with AND +Optional findByEmailAndActive(String email, boolean active); +List findByNameAndAgeGreaterThan(String name, int age); +List findByActiveAndCreatedDateAfter(boolean active, Date date); + +// Generated queries +// SELECT u FROM User u WHERE u.email = :email AND u.active = :active +// SELECT u FROM User u WHERE u.name = :name AND u.age > :age +// SELECT u FROM User u WHERE u.active = :active AND u.createdDate > :date +``` + +#### OR Operations + +```java +// Multiple conditions with OR +List findByNameOrEmail(String name, String email); +List findByActiveOrVerified(boolean active, boolean verified); + +// Generated queries +// SELECT u FROM User u WHERE u.name = :name OR u.email = :email +// SELECT u FROM User u WHERE u.active = :active OR u.verified = :verified +``` + +#### Complex Combinations + +```java +// Complex logical combinations +List findByActiveAndNameContainingOrEmailContaining( + boolean active, String namePattern, String emailPattern); + +// Generated query +// SELECT u FROM User u WHERE u.active = :active AND +// (u.name LIKE %:namePattern% OR u.email LIKE %:emailPattern%) +``` + +### Ordering + +```java +// Single property ordering +List findByActiveOrderByNameAsc(boolean active); +List findByActiveOrderByNameDesc(boolean active); +List findByActiveOrderByCreatedDateDesc(boolean active); + +// Multiple property ordering +List findByActiveOrderByNameAscAgeDesc(boolean active); +List findByDepartmentOrderByNameAscCreatedDateDesc(String department); + +// Generated queries +// SELECT u FROM User u WHERE u.active = :active ORDER BY u.name ASC +// SELECT u FROM User u WHERE u.active = :active ORDER BY u.name DESC +// SELECT u FROM User u WHERE u.active = :active ORDER BY u.name ASC, u.age DESC +``` + +### Return Types + +#### Optional for Single Results + +```java +// Single result that might not exist +Optional findByEmail(String email); +Optional findByIdAndActive(Long id, boolean active); + +// Framework automatically wraps single results in Optional +``` + +#### Lists for Multiple Results + +```java +// Multiple results +List findByActive(boolean active); +List findByAgeGreaterThan(int age); +List findByNameContaining(String pattern); + +// Empty list returned if no results found +``` + +#### Primitive Types for Counts and Checks + +```java +// Count operations +long countByActive(boolean active); +long countByAgeGreaterThan(int age); + +// Existence checks +boolean existsByEmail(String email); +boolean existsByEmailAndActive(String email, boolean active); + +// Delete operations (returns count of deleted entities) +long deleteByActive(boolean active); +int deleteByAgeGreaterThan(int age); +``` + +## Custom Queries with @Query + +For complex queries that can't be expressed through method names: + +### HQL/JPQL Queries + +```java +public interface UserRepository extends BaseRepository { + + // Simple custom query + @Query("SELECT u FROM User u WHERE u.email = :email AND u.active = true") + Optional findActiveUserByEmail(String email); + + // Query with joins + @Query("SELECT u FROM User u JOIN u.department d WHERE d.name = :deptName") + List findUsersByDepartmentName(String deptName); + + // Aggregate queries + @Query("SELECT COUNT(u) FROM User u WHERE u.createdDate > :date") + long countUsersCreatedAfter(Date date); + + @Query("SELECT AVG(u.age) FROM User u WHERE u.active = true") + Double getAverageAgeOfActiveUsers(); + + // Subqueries + @Query(""" + SELECT u FROM User u + WHERE u.id IN ( + SELECT p.user.id FROM Post p + WHERE p.published = true AND p.createdDate > :date + ) + """) + List findUsersWithRecentPublishedPosts(Date date); + + // Complex conditions + @Query(""" + SELECT u FROM User u + WHERE u.active = true + AND u.department.name = :dept + AND u.createdDate BETWEEN :startDate AND :endDate + ORDER BY u.name ASC + """) + List findActiveUsersByDepartmentAndDateRange( + String dept, Date startDate, Date endDate); +} +``` + +### Native SQL Queries + +```java +public interface UserRepository extends BaseRepository { + + // Simple native query + @Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true) + Optional findByEmailNative(String email); + + // Complex native query with joins + @Query(value = """ + SELECT u.*, d.name as dept_name + FROM users u + LEFT JOIN departments d ON u.department_id = d.id + WHERE u.active = true + AND d.name = :deptName + ORDER BY u.name + """, nativeQuery = true) + List findActiveUsersByDepartmentNative(String deptName); + + // Native aggregate query + @Query(value = """ + SELECT COUNT(*) + FROM users u + WHERE u.created_date > :date + AND u.active = true + """, nativeQuery = true) + long countActiveUsersCreatedAfter(Date date); + + // Database-specific features + @Query(value = """ + SELECT u.* FROM users u + WHERE MATCH(u.name, u.email) AGAINST (:searchTerm IN NATURAL LANGUAGE MODE) + """, nativeQuery = true) + List fullTextSearch(String searchTerm); // MySQL specific +} +``` + +### Modifying Queries + +Use `@Modifying` for UPDATE, DELETE, or INSERT operations: + +```java +public interface UserRepository extends BaseRepository { + + // Update operations + @Query("UPDATE User u SET u.active = :active WHERE u.email = :email") + @Modifying + int updateUserActiveStatus(String email, boolean active); + + @Query("UPDATE User u SET u.lastLogin = CURRENT_TIMESTAMP WHERE u.id = :id") + @Modifying + int updateLastLogin(Long id); + + // Bulk updates + @Query("UPDATE User u SET u.active = false WHERE u.lastLogin < :cutoffDate") + @Modifying + int deactivateInactiveUsers(Date cutoffDate); + + @Query("UPDATE User u SET u.verified = true WHERE u.email IN :emails") + @Modifying + int verifyUsersByEmails(List emails); + + // Delete operations + @Query("DELETE FROM User u WHERE u.active = false AND u.createdDate < :cutoffDate") + @Modifying + int deleteOldInactiveUsers(Date cutoffDate); + + // Native modifying queries + @Query(value = "UPDATE users SET login_count = login_count + 1 WHERE id = ?1", + nativeQuery = true) + @Modifying + int incrementLoginCount(Long userId); +} +``` + +## Parameter Binding + +### Named Parameters + +```java +// HQL with named parameters +@Query("SELECT u FROM User u WHERE u.name = :name AND u.age > :minAge") +List findByNameAndMinAge(String name, int minAge); + +// Native SQL with named parameters +@Query(value = "SELECT * FROM users WHERE name = :name AND age > :minAge", + nativeQuery = true) +List findByNameAndMinAgeNative(String name, int minAge); +``` + +### Positional Parameters + +```java +// HQL with positional parameters +@Query("SELECT u FROM User u WHERE u.name = ?1 AND u.age > ?2") +List findByNameAndMinAge(String name, int minAge); + +// Native SQL with positional parameters +@Query(value = "SELECT * FROM users WHERE name = ?1 AND age > ?2", + nativeQuery = true) +List findByNameAndMinAgeNative(String name, int minAge); +``` + +### Collection Parameters + +```java +// Collections in queries +@Query("SELECT u FROM User u WHERE u.status IN :statuses") +List findByStatuses(List statuses); + +@Query("SELECT u FROM User u WHERE u.id IN :ids") +List findByIds(Set ids); + +// Native SQL with collections +@Query(value = "SELECT * FROM users WHERE status IN (:statuses)", nativeQuery = true) +List findByStatusesNative(List statuses); +``` + +## Advanced Query Patterns + +### Pagination and Limiting + +```java +// Top/First keywords for limiting results +List findTop10ByActiveOrderByCreatedDateDesc(boolean active); +List findFirst5ByNameContainingOrderByName(String namePattern); +Optional findFirstByActiveOrderByCreatedDateDesc(boolean active); + +// Custom limit with @Query +@Query(value = "SELECT * FROM users WHERE active = ?1 ORDER BY created_date DESC LIMIT ?2", + nativeQuery = true) +List findActiveUsersWithLimit(boolean active, int limit); +``` + +### Distinct Results + +```java +// Distinct keyword +List findDistinctNameByActive(boolean active); +List findDistinctByDepartmentName(String departmentName); + +// Custom distinct with @Query +@Query("SELECT DISTINCT u.department FROM User u WHERE u.active = true") +List findDistinctActiveDepartments(); +``` + +### Case Insensitive Queries + +```java +// IgnoreCase keyword +List findByNameIgnoreCase(String name); +List findByEmailContainingIgnoreCase(String emailPattern); + +// Custom case insensitive with @Query +@Query("SELECT u FROM User u WHERE UPPER(u.name) = UPPER(:name)") +List findByNameCaseInsensitive(String name); +``` + +### Date and Time Queries + +```java +// Date comparisons +List findByCreatedDateAfter(Date date); +List findByCreatedDateBefore(Date date); +List findByCreatedDateBetween(Date start, Date end); + +// Time-based queries with @Query +@Query("SELECT u FROM User u WHERE u.createdDate >= :startOfDay AND u.createdDate < :endOfDay") +List findUsersCreatedOnDate(Date startOfDay, Date endOfDay); + +@Query("SELECT u FROM User u WHERE YEAR(u.createdDate) = :year") +List findUsersCreatedInYear(int year); + +// Native SQL for database-specific date functions +@Query(value = "SELECT * FROM users WHERE DATE(created_date) = CURDATE()", nativeQuery = true) +List findUsersCreatedToday(); +``` + +## Performance Considerations + +### Index Usage + +```java +// Queries that can use indexes effectively +Optional findByEmail(String email); // If email is indexed +List findByActive(boolean active); // If active is indexed +List findByCreatedDateAfter(Date date); // If created_date is indexed + +// Compound index usage +List findByActiveAndDepartment(boolean active, String department); +// Effective if there's an index on (active, department) +``` + +### Query Optimization + +```java +// Good: Specific queries +List findByActive(boolean active); +long countByActive(boolean active); +boolean existsByEmail(String email); + +// Avoid: Inefficient patterns +// Don't do this - loads all users into memory then filters +// List allUsers = userRepository.findAll(); +// List activeUsers = allUsers.stream() +// .filter(User::isActive) +// .collect(toList()); + +// Do this instead - database-level filtering +List activeUsers = userRepository.findByActive(true); +``` + +### Batch Operations + +```java +// Efficient batch operations +List saveAll(Iterable users); +void deleteAllById(Iterable ids); + +// Bulk operations with @Query +@Query("UPDATE User u SET u.active = false WHERE u.id IN :ids") +@Modifying +int deactivateUsers(List ids); + +@Query("DELETE FROM User u WHERE u.id IN :ids") +@Modifying +int deleteUsersByIds(List ids); +``` + +## Error Handling and Debugging + +### Common Errors + +```java +// Error: Method name doesn't follow conventions +// List getUsersByEmail(String email); // Wrong prefix +List findByEmail(String email); // Correct + +// Error: Property doesn't exist +// List findByNonExistentProperty(String value); // Will fail +List findByName(String name); // Correct + +// Error: Parameter count mismatch +// List findByEmailAndActive(String email); // Missing parameter +List findByEmailAndActive(String email, boolean active); // Correct +``` + +### Debug Query Generation + +Enable SQL logging to see generated queries: + +```properties +# Enable SQL logging +jazzy.jpa.show-sql=true +jazzy.jpa.hibernate.format_sql=true + +# Enable parameter logging +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +``` + +### Testing Query Methods + +```java +@Component +public class UserRepositoryTest { + private final UserRepository userRepository; + + public UserRepositoryTest(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public void testQueryMethods() { + // Test data setup + User user1 = new User("John Doe", "john@example.com", "password"); + User user2 = new User("Jane Smith", "jane@example.com", "password"); + user2.setActive(false); + + userRepository.saveAll(List.of(user1, user2)); + + // Test method name queries + Optional foundByEmail = userRepository.findByEmail("john@example.com"); + assert foundByEmail.isPresent(); + assert foundByEmail.get().getName().equals("John Doe"); + + List activeUsers = userRepository.findByActive(true); + assert activeUsers.size() == 1; + assert activeUsers.get(0).getName().equals("John Doe"); + + long activeCount = userRepository.countByActive(true); + assert activeCount == 1; + + boolean exists = userRepository.existsByEmail("john@example.com"); + assert exists; + + // Test custom queries + Optional activeUser = userRepository.findActiveUserByEmail("john@example.com"); + assert activeUser.isPresent(); + + // Cleanup + userRepository.deleteAll(); + } +} +``` + +## Best Practices + +### 1. Method Naming + +```java +// Good: Clear, descriptive names +Optional findByEmail(String email); +List findActiveUsersByDepartment(String department); +long countUsersByRegistrationDateAfter(Date date); + +// Avoid: Unclear or overly complex names +Optional findByEmailAndActiveAndDepartmentAndRole(...); // Too complex +List getStuff(String thing); // Unclear +``` + +### 2. Return Types + +```java +// Good: Appropriate return types +Optional findByEmail(String email); // Single result that might not exist +List findByActive(boolean active); // Multiple results +boolean existsByEmail(String email); // Existence check +long countByActive(boolean active); // Count operation + +// Avoid: Inappropriate return types +User findByEmail(String email); // Might return null +Optional> findByActive(boolean active); // Unnecessary Optional wrapping +``` + +### 3. Query Complexity + +```java +// Good: Simple, focused queries +@Query("SELECT u FROM User u WHERE u.email = :email AND u.active = true") +Optional findActiveUserByEmail(String email); + +// Consider: Breaking down complex queries +@Query("SELECT u FROM User u WHERE u.department.name = :dept") +List findUsersByDepartment(String dept); + +// Avoid: Overly complex queries +@Query(""" + SELECT u FROM User u + JOIN u.department d + JOIN u.roles r + WHERE u.active = true + AND d.name = :dept + AND r.name IN :roles + AND u.createdDate > :date + """) +List findComplexUserCriteria(...); // Consider breaking this down +``` + +### 4. Performance + +```java +// Good: Use specific queries +boolean existsByEmail(String email); // Better than findByEmail().isPresent() +long countByActive(boolean active); // Better than findByActive().size() + +// Good: Use appropriate indexes +List findByEmail(String email); // Ensure email is indexed +List findByActiveAndDepartment(boolean active, String dept); // Compound index + +// Avoid: Inefficient patterns +List findAll(); // Then filter in memory - use specific queries instead +``` + +## Next Steps + +- [Repository Pattern](repositories.md) - Repository interface design +- [Database Examples](database-examples.md) - Complete working examples +- [Database Integration](database-integration.md) - Overall database setup guide \ No newline at end of file diff --git a/docs-site/docs/repositories.md b/docs-site/docs/repositories.md new file mode 100644 index 0000000..cb29a05 --- /dev/null +++ b/docs-site/docs/repositories.md @@ -0,0 +1,554 @@ +# Repository Pattern + +The Repository Pattern in Jazzy Framework provides a Spring Data JPA-like abstraction for data access. It automatically generates implementations for repository interfaces, eliminating the need for boilerplate code while providing powerful query capabilities. + +## Overview + +Jazzy's repository system provides: + +- **Automatic Implementation**: No need to write repository implementations +- **Type Safety**: Generic type parameters ensure compile-time safety +- **Method Name Parsing**: Automatic query generation from method names +- **Custom Queries**: Support for HQL/JPQL and native SQL +- **Transaction Management**: Automatic transaction handling +- **Caching**: Built-in repository instance caching + +## BaseRepository Interface + +All repository interfaces must extend `BaseRepository`: + +```java +public interface BaseRepository { + // Save operations + T save(T entity); + List saveAll(Iterable entities); + T saveAndFlush(T entity); + + // Find operations + Optional findById(ID id); + List findAll(); + List findAllById(Iterable ids); + + // Existence checks + boolean existsById(ID id); + long count(); + + // Delete operations + void deleteById(ID id); + void delete(T entity); + void deleteAllById(Iterable ids); + void deleteAll(Iterable entities); + void deleteAll(); + void deleteInBatch(Iterable entities); + void deleteAllInBatch(); + + // Utility operations + void flush(); +} +``` + +## Creating Repository Interfaces + +### Basic Repository + +```java +package com.example.repository; + +import com.example.entity.User; +import jazzyframework.data.BaseRepository; + +public interface UserRepository extends BaseRepository { + // Inherits all basic CRUD operations + // Additional methods can be added here +} +``` + +### Repository with Custom Methods + +```java +package com.example.repository; + +import com.example.entity.User; +import jazzyframework.data.BaseRepository; +import jazzyframework.data.annotations.Query; +import jazzyframework.data.annotations.Modifying; + +import java.util.List; +import java.util.Optional; + +public interface UserRepository extends BaseRepository { + + // Method name parsing - automatically generates queries + Optional findByEmail(String email); + List findByActive(boolean active); + List findByNameContaining(String name); + List findByAgeGreaterThan(int age); + List findByEmailAndActive(String email, boolean active); + + // Count operations + long countByActive(boolean active); + long countByAgeGreaterThan(int age); + + // Existence checks + boolean existsByEmail(String email); + boolean existsByEmailAndActive(String email, boolean active); + + // Custom HQL/JPQL queries + @Query("SELECT u FROM User u WHERE u.email = :email AND u.active = true") + Optional findActiveUserByEmail(String email); + + @Query("SELECT u FROM User u WHERE u.name LIKE %:name% ORDER BY u.name") + List searchByNameSorted(String name); + + // Native SQL queries + @Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true) + Optional findByEmailNative(String email); + + // Update operations + @Query("UPDATE User u SET u.active = :active WHERE u.email = :email") + @Modifying + int updateUserActiveStatus(String email, boolean active); + + @Query("DELETE FROM User u WHERE u.active = false") + @Modifying + int deleteInactiveUsers(); +} +``` + +## Method Name Query Generation + +Jazzy automatically generates queries based on method names following Spring Data JPA conventions: + +### Supported Keywords + +| Keyword | Sample | JPQL snippet | +|---------|--------|--------------| +| `And` | `findByLastnameAndFirstname` | `… where x.lastname = ?1 and x.firstname = ?2` | +| `Or` | `findByLastnameOrFirstname` | `… where x.lastname = ?1 or x.firstname = ?2` | +| `Is`, `Equals` | `findByFirstname`, `findByFirstnameIs` | `… where x.firstname = ?1` | +| `Between` | `findByStartDateBetween` | `… where x.startDate between ?1 and ?2` | +| `LessThan` | `findByAgeLessThan` | `… where x.age < ?1` | +| `LessThanEqual` | `findByAgeLessThanEqual` | `… where x.age <= ?1` | +| `GreaterThan` | `findByAgeGreaterThan` | `… where x.age > ?1` | +| `GreaterThanEqual` | `findByAgeGreaterThanEqual` | `… where x.age >= ?1` | +| `After` | `findByStartDateAfter` | `… where x.startDate > ?1` | +| `Before` | `findByStartDateBefore` | `… where x.startDate < ?1` | +| `IsNull`, `Null` | `findByAge(Is)Null` | `… where x.age is null` | +| `IsNotNull`, `NotNull` | `findByAge(Is)NotNull` | `… where x.age not null` | +| `Like` | `findByFirstnameLike` | `… where x.firstname like ?1` | +| `NotLike` | `findByFirstnameNotLike` | `… where x.firstname not like ?1` | +| `StartingWith` | `findByFirstnameStartingWith` | `… where x.firstname like ?1%` | +| `EndingWith` | `findByFirstnameEndingWith` | `… where x.firstname like %?1` | +| `Containing` | `findByFirstnameContaining` | `… where x.firstname like %?1%` | +| `OrderBy` | `findByAgeOrderByLastnameDesc` | `… where x.age = ?1 order by x.lastname desc` | +| `Not` | `findByLastnameNot` | `… where x.lastname <> ?1` | +| `In` | `findByAgeIn(Collection ages)` | `… where x.age in ?1` | +| `NotIn` | `findByAgeNotIn(Collection ages)` | `… where x.age not in ?1` | +| `True` | `findByActiveTrue()` | `… where x.active = true` | +| `False` | `findByActiveFalse()` | `… where x.active = false` | +| `IgnoreCase` | `findByFirstnameIgnoreCase` | `… where UPPER(x.firstname) = UPPER(?1)` | + +### Query Method Examples + +```java +public interface UserRepository extends BaseRepository { + + // Simple property queries + Optional findByEmail(String email); + List findByActive(boolean active); + List findByName(String name); + + // Comparison queries + List findByAgeGreaterThan(int age); + List findByAgeLessThanEqual(int age); + List findByAgeBetween(int minAge, int maxAge); + + // String queries + List findByNameContaining(String name); + List findByNameStartingWith(String prefix); + List findByNameEndingWith(String suffix); + List findByNameLike(String pattern); + + // Boolean queries + List findByActiveTrue(); + List findByActiveFalse(); + + // Null checks + List findByLastLoginIsNull(); + List findByLastLoginIsNotNull(); + + // Collection queries + List findByAgeIn(Collection ages); + List findByAgeNotIn(Collection ages); + + // Combined conditions + List findByEmailAndActive(String email, boolean active); + List findByNameOrEmail(String name, String email); + List findByActiveAndAgeGreaterThan(boolean active, int age); + + // Ordering + List findByActiveOrderByNameAsc(boolean active); + List findByActiveOrderByNameDesc(boolean active); + List findByActiveOrderByNameAscAgeDesc(boolean active); + + // Count queries + long countByActive(boolean active); + long countByAgeGreaterThan(int age); + long countByEmailContaining(String emailPart); + + // Existence queries + boolean existsByEmail(String email); + boolean existsByEmailAndActive(String email, boolean active); + + // Delete queries + void deleteByActive(boolean active); + void deleteByAgeGreaterThan(int age); + long deleteByEmailContaining(String emailPart); // Returns count +} +``` + +## Custom Queries with @Query + +For complex queries that can't be expressed through method names, use the `@Query` annotation: + +### HQL/JPQL Queries + +```java +public interface UserRepository extends BaseRepository { + + // Simple HQL query + @Query("SELECT u FROM User u WHERE u.email = :email") + Optional findByEmailHql(String email); + + // Query with multiple parameters + @Query("SELECT u FROM User u WHERE u.name = :name AND u.age > :minAge") + List findByNameAndMinAge(String name, int minAge); + + // Query with LIKE operator + @Query("SELECT u FROM User u WHERE u.name LIKE %:name% ORDER BY u.name") + List searchByName(String name); + + // Aggregate queries + @Query("SELECT COUNT(u) FROM User u WHERE u.active = :active") + long countActiveUsers(boolean active); + + @Query("SELECT AVG(u.age) FROM User u WHERE u.active = true") + Double getAverageAgeOfActiveUsers(); + + // Join queries + @Query("SELECT u FROM User u JOIN u.posts p WHERE p.title LIKE %:title%") + List findUsersByPostTitle(String title); + + // Subqueries + @Query("SELECT u FROM User u WHERE u.id IN (SELECT p.user.id FROM Post p WHERE p.published = true)") + List findUsersWithPublishedPosts(); +} +``` + +### Native SQL Queries + +```java +public interface UserRepository extends BaseRepository { + + // Simple native query + @Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true) + Optional findByEmailNative(String email); + + // Native query with named parameters + @Query(value = "SELECT * FROM users WHERE name = :name AND active = :active", nativeQuery = true) + List findByNameAndActiveNative(String name, boolean active); + + // Complex native query + @Query(value = """ + SELECT u.* FROM users u + LEFT JOIN posts p ON u.id = p.user_id + WHERE u.active = true + GROUP BY u.id + HAVING COUNT(p.id) > :minPosts + ORDER BY COUNT(p.id) DESC + """, nativeQuery = true) + List findActiveUsersWithMinimumPosts(int minPosts); + + // Native count query + @Query(value = "SELECT COUNT(*) FROM users WHERE active = ?1", nativeQuery = true) + long countByActiveNative(boolean active); +} +``` + +## Modifying Queries + +Use `@Modifying` annotation for UPDATE, DELETE, or INSERT operations: + +```java +public interface UserRepository extends BaseRepository { + + // Update operations + @Query("UPDATE User u SET u.active = :active WHERE u.email = :email") + @Modifying + int updateUserActiveStatus(String email, boolean active); + + @Query("UPDATE User u SET u.lastLogin = CURRENT_TIMESTAMP WHERE u.id = :id") + @Modifying + int updateLastLogin(Long id); + + // Bulk update + @Query("UPDATE User u SET u.active = false WHERE u.lastLogin < :cutoffDate") + @Modifying + int deactivateInactiveUsers(Date cutoffDate); + + // Delete operations + @Query("DELETE FROM User u WHERE u.active = false") + @Modifying + int deleteInactiveUsers(); + + @Query("DELETE FROM User u WHERE u.lastLogin < :cutoffDate") + @Modifying + int deleteOldUsers(Date cutoffDate); + + // Native modifying queries + @Query(value = "UPDATE users SET active = false WHERE last_login < ?1", nativeQuery = true) + @Modifying + int deactivateOldUsersNative(Date cutoffDate); +} +``` + +## Repository Implementation Details + +### Automatic Proxy Creation + +Jazzy automatically creates proxy implementations for repository interfaces: + +```java +// Framework automatically creates this implementation +public class UserRepositoryImpl implements UserRepository { + private final SessionFactory sessionFactory; + private final BaseRepositoryImpl baseImpl; + + // All methods are automatically implemented + public Optional findByEmail(String email) { + // Generated query: SELECT u FROM User u WHERE u.email = :email + // Automatic parameter binding and result mapping + } +} +``` + +### Transaction Management + +Each repository method automatically runs in a transaction: + +```java +// Each method call is wrapped in a transaction +userRepository.save(user); // Transaction: BEGIN -> SAVE -> COMMIT +userRepository.findByEmail(email); // Transaction: BEGIN -> SELECT -> COMMIT +userRepository.deleteById(id); // Transaction: BEGIN -> DELETE -> COMMIT +``` + +### Error Handling + +Repository methods provide automatic error handling: + +```java +try { + User user = userRepository.save(user); + return user; +} catch (IllegalArgumentException e) { + // Validation errors (null parameters, etc.) + throw e; +} catch (Exception e) { + // Database errors are wrapped in RuntimeException + throw new RuntimeException("Database operation failed", e); +} +``` + +## Advanced Repository Patterns + +### Repository with Custom Base + +```java +// Custom base repository with additional methods +public interface CustomBaseRepository extends BaseRepository { + List findAllActive(); + void softDelete(ID id); + void restore(ID id); +} + +// Implementation would be provided by extending BaseRepositoryImpl +public interface UserRepository extends CustomBaseRepository { + Optional findByEmail(String email); +} +``` + +### Repository Composition + +```java +// Separate repositories for different concerns +public interface UserRepository extends BaseRepository { + Optional findByEmail(String email); + List findByActive(boolean active); +} + +public interface UserSecurityRepository extends BaseRepository { + @Query("UPDATE User u SET u.password = :password WHERE u.id = :id") + @Modifying + int updatePassword(Long id, String password); + + @Query("UPDATE User u SET u.failedLoginAttempts = :attempts WHERE u.id = :id") + @Modifying + int updateFailedLoginAttempts(Long id, int attempts); +} + +// Service layer can inject both repositories +@Component +public class UserService { + private final UserRepository userRepository; + private final UserSecurityRepository userSecurityRepository; + + public UserService(UserRepository userRepository, + UserSecurityRepository userSecurityRepository) { + this.userRepository = userRepository; + this.userSecurityRepository = userSecurityRepository; + } +} +``` + +## Best Practices + +### 1. Repository Naming + +```java +// Good: Clear, specific names +public interface UserRepository extends BaseRepository {} +public interface OrderRepository extends BaseRepository {} +public interface ProductRepository extends BaseRepository {} + +// Avoid: Generic or unclear names +public interface DataRepository extends BaseRepository {} +public interface Repository extends BaseRepository {} +``` + +### 2. Method Naming + +```java +// Good: Descriptive method names +Optional findByEmail(String email); +List findActiveUsersByDepartment(String department); +long countUsersByRegistrationDateAfter(Date date); + +// Avoid: Unclear or overly complex names +Optional findByEmailAndActiveAndDepartmentAndRoleAndStatusAndCreatedDateAfter(...); +List getUserStuff(String thing); +``` + +### 3. Query Complexity + +```java +// Good: Simple, focused queries +@Query("SELECT u FROM User u WHERE u.email = :email AND u.active = true") +Optional findActiveUserByEmail(String email); + +// Consider breaking down complex queries +@Query("SELECT u FROM User u WHERE u.department.name = :dept AND u.active = true") +List findActiveUsersByDepartment(String dept); + +// Avoid: Overly complex single queries +@Query(""" + SELECT u FROM User u + JOIN u.department d + JOIN u.roles r + JOIN u.permissions p + WHERE u.active = true + AND d.name = :dept + AND r.name IN :roles + AND p.name IN :permissions + AND u.createdDate > :date + AND u.lastLogin IS NOT NULL + """) +List findComplexUserCriteria(...); // Too complex, break it down +``` + +### 4. Return Types + +```java +// Good: Appropriate return types +Optional findByEmail(String email); // Single result that might not exist +List findByActive(boolean active); // Multiple results +boolean existsByEmail(String email); // Existence check +long countByActive(boolean active); // Count operation + +// Avoid: Inappropriate return types +User findByEmail(String email); // Might return null +Optional> findByActive(boolean active); // Unnecessary Optional wrapping +``` + +## Testing Repositories + +### Unit Testing + +```java +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + @Mock + private UserRepository userRepository; + + @InjectMocks + private UserService userService; + + @Test + void shouldCreateUser() { + // Given + User user = new User("John", "john@example.com", "password"); + when(userRepository.existsByEmail("john@example.com")).thenReturn(false); + when(userRepository.save(any(User.class))).thenReturn(user); + + // When + User result = userService.createUser("John", "john@example.com", "password"); + + // Then + assertThat(result.getName()).isEqualTo("John"); + verify(userRepository).existsByEmail("john@example.com"); + verify(userRepository).save(any(User.class)); + } +} +``` + +### Integration Testing + +```java +@Component +public class UserRepositoryTest { + private final UserRepository userRepository; + + public UserRepositoryTest(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public void testRepositoryOperations() { + // Create test data + User user = new User("Test User", "test@example.com", "password"); + User saved = userRepository.save(user); + + // Test find operations + Optional found = userRepository.findByEmail("test@example.com"); + assert found.isPresent(); + assert found.get().getName().equals("Test User"); + + // Test count operations + long count = userRepository.countByActive(true); + assert count > 0; + + // Test existence checks + boolean exists = userRepository.existsByEmail("test@example.com"); + assert exists; + + // Cleanup + userRepository.delete(saved); + } +} +``` + +## Next Steps + +- [Query Methods](query-methods.md) - Detailed guide to method name parsing +- [Database Examples](database-examples.md) - Complete working examples +- [Database Integration](database-integration.md) - Overall database setup guide \ No newline at end of file diff --git a/docs-site/sidebars.js b/docs-site/sidebars.js index f77355c..6c5521f 100644 --- a/docs-site/sidebars.js +++ b/docs-site/sidebars.js @@ -15,21 +15,56 @@ @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { - // By default, Docusaurus generates a sidebar from the docs folder structure - tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], - - // But you can create a sidebar manually - /* + // Manual sidebar with organized categories tutorialSidebar: [ - 'intro', - 'hello', + // Getting Started + 'index', + 'getting-started', + + // Core Concepts + { + type: 'category', + label: '🏗️ Core Framework', + items: [ + 'routing', + 'requests', + 'responses', + 'response_factory', + 'validation', + ], + }, + + // Dependency Injection + { + type: 'category', + label: '🔧 Dependency Injection', + items: [ + 'dependency-injection', + 'di-examples', + ], + }, + + // Database Integration (NEW in 0.3.0) + { + type: 'category', + label: '🗄️ Database Integration', + items: [ + 'database-integration', + 'repositories', + 'query-methods', + ], + }, + + // Utilities & Advanced { type: 'category', - label: 'Tutorial', - items: ['tutorial-basics/create-a-document'], + label: '🛠️ Utilities & Advanced', + items: [ + 'json', + 'examples', + ], }, ], - */ }; export default sidebars; diff --git a/pom.xml b/pom.xml index 4be5256..01e6288 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.jazzyframework jazzy-framework - 0.2.0 + 0.3.0 Jazzy Framework A lightweight web framework for Java with a low learning curve @@ -39,6 +39,21 @@ 5.8.0 test + + + + org.mockito + mockito-core + 5.7.0 + test + + + org.mockito + mockito-junit-jupiter + 5.7.0 + test + + org.slf4j slf4j-api @@ -57,6 +72,43 @@ picocontainer 2.15 + + + + org.hibernate + hibernate-core + 6.4.1.Final + + + + + com.zaxxer + HikariCP + 5.1.0 + + + + + com.h2database + h2 + 2.2.224 + + + + + mysql + mysql-connector-java + 8.0.33 + runtime + + + + + org.postgresql + postgresql + 42.7.1 + runtime + diff --git a/src/main/java/examples/database/DatabaseExampleApp.java b/src/main/java/examples/database/DatabaseExampleApp.java new file mode 100644 index 0000000..340a510 --- /dev/null +++ b/src/main/java/examples/database/DatabaseExampleApp.java @@ -0,0 +1,86 @@ +package examples.database; + +import examples.database.controller.UserController; +import examples.database.entity.User; +import examples.database.service.UserService; +import jazzyframework.core.Config; +import jazzyframework.core.Server; +import jazzyframework.routing.Router; + +import java.util.List; +import java.util.logging.Logger; + +/** + * Example application demonstrating database integration with JazzyFramework. + * + *

This application showcases: + *

    + *
  • Zero-configuration database setup with H2
  • + *
  • Automatic entity discovery and registration
  • + *
  • Automatic repository implementation
  • + *
  • Dependency injection with repositories and services
  • + *
  • RESTful API endpoints for database operations
  • + *
+ * + *

The application automatically: + *

    + *
  1. Reads configuration from application.properties
  2. + *
  3. Initializes H2 database and Hibernate
  4. + *
  5. Discovers and registers User entity
  6. + *
  7. Creates UserRepository implementation
  8. + *
  9. Injects dependencies into UserService and UserController
  10. + *
  11. Registers REST endpoints for user management
  12. + *
+ * + *

Available endpoints: + *

    + *
  • GET /users - list all users
  • + *
  • GET /users/{id} - get user by ID
  • + *
  • GET /users/search?email=... - find user by email
  • + *
  • GET /users/active - list active users
  • + *
  • GET /users/age-range?min=...&max=... - find users by age range
  • + *
  • POST /users - create new user
  • + *
  • PUT /users/{id} - update user
  • + *
  • DELETE /users/{id} - delete user
  • + *
  • PUT /users/{id}/deactivate - deactivate user
  • + *
+ * + *

H2 Console is available at: http://localhost:8082 + * + * @since 0.3.0 + * @author Caner Mastan + */ +public class DatabaseExampleApp { + private static final Logger logger = Logger.getLogger(DatabaseExampleApp.class.getName()); + + public static void main(String[] args) { + logger.info("Starting JazzyFramework Database Example Application..."); + + try { + Config config = new Config(); + config.setServerPort(8080); + config.setEnableMetrics(true); + + Router router = new Router(); + + // User routes - all endpoint definitions for the UserController + router.GET("/users", "getAllUsers", UserController.class); + router.GET("/users/{id}", "getUserById", UserController.class); + router.GET("/users/search", "searchUserByEmail", UserController.class); + router.GET("/users/active", "getActiveUsers", UserController.class); + router.GET("/users/age-range", "getUsersByAgeRange", UserController.class); + router.POST("/users", "createUser", UserController.class); + router.PUT("/users/{id}", "updateUser", UserController.class); + router.PUT("/users/{id}/deactivate", "deactivateUser", UserController.class); + router.DELETE("/users/{id}", "deleteUser", UserController.class); + + Server server = new Server(router, config); + + server.start(config.getServerPort()); + } catch (Exception e) { + logger.severe("Failed to start application: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } +} \ No newline at end of file diff --git a/src/main/java/examples/database/controller/UserController.java b/src/main/java/examples/database/controller/UserController.java new file mode 100644 index 0000000..0e3d181 --- /dev/null +++ b/src/main/java/examples/database/controller/UserController.java @@ -0,0 +1,210 @@ +package examples.database.controller; + +import examples.database.entity.User; +import examples.database.service.UserService; +import jazzyframework.http.JSON; +import jazzyframework.http.Request; +import jazzyframework.http.Response; +import jazzyframework.http.validation.ValidationResult; +import jazzyframework.http.validation.Validator; +import jazzyframework.di.annotations.Component; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * REST controller for User management operations. + * + *

This controller demonstrates: + *

    + *
  • RESTful API design with proper HTTP methods
  • + *
  • Dependency injection with service layer
  • + *
  • Request validation using framework validators
  • + *
  • JSON responses with proper HTTP status codes
  • + *
  • Error handling and user feedback
  • + *
+ * + *

Available endpoints (must be registered in main app): + *

    + *
  • GET /users - list all users
  • + *
  • GET /users/{id} - get user by ID
  • + *
  • GET /users/search?email=... - find user by email
  • + *
  • GET /users/active - list active users
  • + *
  • GET /users/age-range?min=...&max=... - find users by age range
  • + *
  • POST /users - create new user
  • + *
  • PUT /users/{id} - update user
  • + *
  • DELETE /users/{id} - delete user
  • + *
  • PUT /users/{id}/deactivate - deactivate user
  • + *
+ * + * @since 0.3.0 + * @author Caner Mastan + */ +@Component +public class UserController { + private static final Logger logger = Logger.getLogger(UserController.class.getName()); + + private final UserService userService; + + /** + * Creates a new UserController. + * UserService will be automatically injected by the DI container. + * + * @param userService the user service + */ + public UserController(UserService userService) { + this.userService = userService; + logger.info("UserController created with injected UserService"); + } + + /** + * Lists all users. + * + * @param request the HTTP request + * @return JSON response with user list + */ + public Response getAllUsers(Request request) { + logger.info("GET /users - Fetching all users"); + + try { + List users = userService.findAllUsers(); + long totalCount = userService.getUserCount(); + + Map response = Map.of( + "users", users, + "total", totalCount, + "count", users.size() + ); + + return Response.json(response); + } catch (Exception e) { + logger.severe("Error fetching users: " + e.getMessage()); + return Response.json(Map.of("error", "Failed to fetch users")).status(500); + } + } + + /** + * Gets a user by ID. + * + * @param request the HTTP request + * @return JSON response with user data or 404 if not found + */ + public Response getUserById(Request request) { + String idParam = request.path("id"); + logger.info("GET /users/" + idParam + " - Fetching user by ID"); + + try { + Long id = Long.parseLong(idParam); + Optional user = userService.findById(id); + + if (user.isPresent()) { + return Response.json(Map.of("user", user.get())); + } else { + return Response.json(Map.of("error", "User not found with ID: " + id)).status(404); + } + } catch (NumberFormatException e) { + return Response.json(Map.of("error", "Invalid user ID format")).status(400); + } catch (Exception e) { + logger.severe("Error fetching user: " + e.getMessage()); + return Response.json(Map.of("error", "Failed to fetch user")).status(500); + } + } + + /** + * Creates a new user. + * + * @param request the HTTP request + * @return JSON response with created user data + */ + public Response createUser(Request request) { + logger.info("POST /users - Creating new user"); + + User user = request.toObject(User.class); + User createdUser = userService.createUser(user.getName(), user.getEmail(), user.getPassword()); + return Response.json(JSON.of("result", createdUser)); + } + + /** + * Searches for a user by email. + * + * @param request the HTTP request + * @return JSON response with user data or 404 if not found + */ + public Response searchUserByEmail(Request request) { + String email = request.query("email"); + logger.info("GET /users/search?email=" + email + " - Searching user by email"); + + if (email == null || email.trim().isEmpty()) { + return Response.json(Map.of("error", "Email parameter is required")).status(400); + } + + try { + Optional user = userService.findByEmail(email.trim()); + + if (user.isPresent()) { + return Response.json(Map.of("user", user.get())); + } else { + return Response.json(Map.of("error", "User not found with email: " + email)).status(404); + } + } catch (Exception e) { + logger.severe("Error searching user: " + e.getMessage()); + return Response.json(Map.of("error", "Failed to search user")).status(500); + } + } + + /** + * Deactivates a user. + * + * @param request the HTTP request + * @return JSON response with success message + */ + public Response deactivateUser(Request request) { + String idParam = request.path("id"); + logger.info("PUT /users/" + idParam + "/deactivate - Deactivating user"); + + try { + Long id = Long.parseLong(idParam); + boolean success = userService.deactivateUser(id); + + if (success) { + return Response.json(Map.of("message", "User deactivated successfully")); + } else { + return Response.json(Map.of("error", "User not found with ID: " + id)).status(404); + } + } catch (NumberFormatException e) { + return Response.json(Map.of("error", "Invalid user ID format")).status(400); + } catch (Exception e) { + logger.severe("Error deactivating user: " + e.getMessage()); + return Response.json(Map.of("error", "Failed to deactivate user")).status(500); + } + } + + /** + * Deletes a user. + * + * @param request the HTTP request + * @return JSON response with success message + */ + public Response deleteUser(Request request) { + String idParam = request.path("id"); + logger.info("DELETE /users/" + idParam + " - Deleting user"); + + try { + Long id = Long.parseLong(idParam); + boolean success = userService.deleteUser(id); + + if (success) { + return Response.json(Map.of("message", "User deleted successfully")); + } else { + return Response.json(Map.of("error", "User not found with ID: " + id)).status(404); + } + } catch (NumberFormatException e) { + return Response.json(Map.of("error", "Invalid user ID format")).status(400); + } catch (Exception e) { + logger.severe("Error deleting user: " + e.getMessage()); + return Response.json(Map.of("error", "Failed to delete user")).status(500); + } + } +} \ No newline at end of file diff --git a/src/main/java/examples/database/entity/User.java b/src/main/java/examples/database/entity/User.java new file mode 100644 index 0000000..f4f7d52 --- /dev/null +++ b/src/main/java/examples/database/entity/User.java @@ -0,0 +1,197 @@ +package examples.database.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +/** + * User entity demonstrating JPA annotations and database integration. + * + *

This entity showcases: + *

    + *
  • Basic JPA annotations (@Entity, @Id, @GeneratedValue)
  • + *
  • Column constraints and specifications
  • + *
  • Timestamp fields with automatic management
  • + *
  • Validation constraints
  • + *
+ * + * @since 0.3.0 + * @author Caner Mastan + */ +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "name", nullable = false, length = 100) + private String name; + + @Column(name = "email", nullable = false, unique = true, length = 255) + private String email; + + @Column(name = "password", nullable = false) + private String password; + + @Column(name = "active", nullable = false) + private Boolean active = true; + + @Column(name = "age") + private Integer age; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + /** + * Default constructor required by JPA. + */ + public User() { + this.createdAt = LocalDateTime.now(); + } + + /** + * Constructor with required fields. + * + * @param name the user's name + * @param email the user's email + * @param password the user's password + */ + public User(String name, String email, String password) { + this(); + this.name = name; + this.email = email; + this.password = password; + } + + /** + * Constructor with all fields. + * + * @param name the user's name + * @param email the user's email + * @param password the user's password + * @param age the user's age + */ + public User(String name, String email, String password, Integer age) { + this(name, email, password); + this.age = age; + } + + /** + * Called before entity update. + */ + @PreUpdate + public void preUpdate() { + this.updatedAt = LocalDateTime.now(); + } + + /** + * Called before entity persistence. + */ + @PrePersist + public void prePersist() { + if (this.createdAt == null) { + this.createdAt = LocalDateTime.now(); + } + if (this.active == null) { + this.active = true; + } + } + + // Getters and Setters + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", name='" + name + '\'' + + ", email='" + email + '\'' + + ", active=" + active + + ", age=" + age + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + User user = (User) o; + + return id != null ? id.equals(user.id) : user.id == null; + } + + @Override + public int hashCode() { + return id != null ? id.hashCode() : 0; + } +} \ No newline at end of file diff --git a/src/main/java/examples/database/repository/UserRepository.java b/src/main/java/examples/database/repository/UserRepository.java new file mode 100644 index 0000000..c422086 --- /dev/null +++ b/src/main/java/examples/database/repository/UserRepository.java @@ -0,0 +1,50 @@ +package examples.database.repository; + +import examples.database.entity.User; +import jazzyframework.data.BaseRepository; + +import java.util.List; +import java.util.Optional; + +/** + * Repository interface for User entity operations. + * + *

This interface demonstrates how to create repository interfaces + * that automatically get implementation through the framework's + * repository system. Simply extend BaseRepository and the framework + * will provide all CRUD operations automatically. + * + *

You can add custom query methods here if needed: + *

    + *
  • findByEmail - find user by email address
  • + *
  • findByActiveTrue - find all active users
  • + *
  • findByAgeBetween - find users within age range
  • + *
+ * + *

Usage example: + *

+ * @Component
+ * public class UserService {
+ *     private final UserRepository userRepository;
+ *     
+ *     public UserService(UserRepository userRepository) {
+ *         this.userRepository = userRepository;
+ *     }
+ *     
+ *     public User createUser(String name, String email, String password) {
+ *         User user = new User(name, email, password);
+ *         return userRepository.save(user);
+ *     }
+ *     
+ *     public Optional<User> findByEmail(String email) {
+ *         return userRepository.findByEmail(email);
+ *     }
+ * }
+ * 
+ * + * @since 0.3.0 + * @author Caner Mastan + */ +public interface UserRepository extends BaseRepository { + Optional findByEmail(String email); +} \ No newline at end of file diff --git a/src/main/java/examples/database/service/UserService.java b/src/main/java/examples/database/service/UserService.java new file mode 100644 index 0000000..42352bb --- /dev/null +++ b/src/main/java/examples/database/service/UserService.java @@ -0,0 +1,236 @@ +package examples.database.service; + +import examples.database.entity.User; +import examples.database.repository.UserRepository; +import jazzyframework.di.annotations.Component; + +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * Service layer for User operations. + * + *

This service demonstrates: + *

    + *
  • Dependency injection with automatically generated repository
  • + *
  • Business logic separation from controller layer
  • + *
  • Transaction management through repository layer
  • + *
  • Data validation and processing
  • + *
+ * + * @since 0.3.0 + * @author Caner Mastan + */ +@Component +public class UserService { + private static final Logger logger = Logger.getLogger(UserService.class.getName()); + + private final UserRepository userRepository; + + /** + * Creates a new UserService. + * UserRepository will be automatically injected by the DI container. + * + * @param userRepository the user repository + */ + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + logger.info("UserService created with injected UserRepository"); + } + + /** + * Creates a new user. + * + * @param name the user's name + * @param email the user's email + * @param password the user's password + * @return the created user + * @throws IllegalArgumentException if email already exists + */ + public User createUser(String name, String email, String password) { + logger.info("Creating user with email: " + email); + + // Check if email already exists + Optional existingUser = userRepository.findByEmail(email); + if (existingUser.isPresent()) { + throw new IllegalArgumentException("User with email " + email + " already exists"); + } + + // Create and save the user + User user = new User(name, email, password); + User savedUser = userRepository.save(user); + + logger.info("User created successfully with ID: " + savedUser.getId()); + return savedUser; + } + + /** + * Creates a new user with age. + * + * @param name the user's name + * @param email the user's email + * @param password the user's password + * @param age the user's age + * @return the created user + * @throws IllegalArgumentException if email already exists or age is invalid + */ + public User createUser(String name, String email, String password, Integer age) { + if (age != null && (age < 0 || age > 150)) { + throw new IllegalArgumentException("Age must be between 0 and 150"); + } + + logger.info("Creating user with email: " + email + " and age: " + age); + + // Check if email already exists + Optional existingUser = userRepository.findByEmail(email); + if (existingUser.isPresent()) { + throw new IllegalArgumentException("User with email " + email + " already exists"); + } + + // Create and save the user + User user = new User(name, email, password, age); + User savedUser = userRepository.save(user); + + logger.info("User created successfully with ID: " + savedUser.getId()); + return savedUser; + } + + /** + * Finds a user by ID. + * + * @param id the user ID + * @return Optional containing the user if found + */ + public Optional findById(Long id) { + logger.fine("Finding user by ID: " + id); + return userRepository.findById(id); + } + + /** + * Finds a user by email. + * + * @param email the email address + * @return Optional containing the user if found + */ + public Optional findByEmail(String email) { + logger.fine("Finding user by email: " + email); + return userRepository.findByEmail(email); + } + + /** + * Finds all users. + * + * @return List of all users + */ + public List findAllUsers() { + logger.fine("Finding all users"); + return userRepository.findAll(); + } + + /** + * Updates a user. + * + * @param id the user ID + * @param name the new name + * @param email the new email + * @param age the new age + * @return Optional containing the updated user if found + * @throws IllegalArgumentException if email already exists for another user + */ + public Optional updateUser(Long id, String name, String email, Integer age) { + logger.info("Updating user with ID: " + id); + + Optional userOpt = userRepository.findById(id); + if (userOpt.isEmpty()) { + logger.warning("User not found with ID: " + id); + return Optional.empty(); + } + + User user = userOpt.get(); + + // Check if email is being changed and if new email already exists + if (!user.getEmail().equals(email)) { + Optional existingUser = userRepository.findByEmail(email); + if (existingUser.isPresent() && !existingUser.get().getId().equals(id)) { + throw new IllegalArgumentException("Email " + email + " is already in use by another user"); + } + } + + // Validate age if provided + if (age != null && (age < 0 || age > 150)) { + throw new IllegalArgumentException("Age must be between 0 and 150"); + } + + // Update user fields + user.setName(name); + user.setEmail(email); + user.setAge(age); + + User updatedUser = userRepository.save(user); + logger.info("User updated successfully with ID: " + updatedUser.getId()); + + return Optional.of(updatedUser); + } + + /** + * Deactivates a user (sets active to false). + * + * @param id the user ID + * @return true if user was deactivated, false if not found + */ + public boolean deactivateUser(Long id) { + logger.info("Deactivating user with ID: " + id); + + Optional userOpt = userRepository.findById(id); + if (userOpt.isEmpty()) { + logger.warning("User not found with ID: " + id); + return false; + } + + User user = userOpt.get(); + user.setActive(false); + userRepository.save(user); + + logger.info("User deactivated successfully with ID: " + id); + return true; + } + + /** + * Permanently deletes a user. + * + * @param id the user ID + * @return true if user was deleted, false if not found + */ + public boolean deleteUser(Long id) { + logger.info("Deleting user with ID: " + id); + + if (!userRepository.existsById(id)) { + logger.warning("User not found with ID: " + id); + return false; + } + + userRepository.deleteById(id); + logger.info("User deleted successfully with ID: " + id); + return true; + } + + /** + * Gets the total number of users. + * + * @return the user count + */ + public long getUserCount() { + return userRepository.count(); + } + + /** + * Checks if a user exists by email. + * + * @param email the email address + * @return true if user exists with the given email + */ + public boolean existsByEmail(String email) { + return userRepository.findByEmail(email).isPresent(); + } +} \ No newline at end of file diff --git a/src/main/java/jazzyframework/core/PropertyLoader.java b/src/main/java/jazzyframework/core/PropertyLoader.java new file mode 100644 index 0000000..0beba12 --- /dev/null +++ b/src/main/java/jazzyframework/core/PropertyLoader.java @@ -0,0 +1,273 @@ +package jazzyframework.core; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.logging.Logger; + +/** + * Loads configuration properties from application.properties file. + * Provides JazzyFramework's own property management with default values and type conversion. + * + *

This class automatically loads properties from: + *

    + *
  • application.properties in classpath root
  • + *
  • application.properties in resources folder
  • + *
  • application.properties in config folder
  • + *
+ * + *

Supports JazzyFramework property patterns: + *

    + *
  • Database configuration (jazzy.datasource.*)
  • + *
  • JPA/Hibernate configuration (jazzy.jpa.*)
  • + *
  • H2 Console configuration (jazzy.h2.*)
  • + *
  • Framework configuration (jazzy.database.*)
  • + *
+ * + *

Example usage: + *

+ * PropertyLoader loader = PropertyLoader.getInstance();
+ * String dbUrl = loader.getDatabaseUrl();
+ * boolean showSql = loader.isShowSql();
+ * 
+ * + * @since 0.3.0 + * @author Caner Mastan + */ +public class PropertyLoader { + private static final Logger logger = Logger.getLogger(PropertyLoader.class.getName()); + private static PropertyLoader instance; + private final Properties properties; + + /** + * Private constructor for singleton pattern. + */ + private PropertyLoader() { + this.properties = new Properties(); + loadProperties(); + } + + /** + * Gets the singleton instance of PropertyLoader. + * + * @return the PropertyLoader instance + */ + public static PropertyLoader getInstance() { + if (instance == null) { + synchronized (PropertyLoader.class) { + if (instance == null) { + instance = new PropertyLoader(); + } + } + } + return instance; + } + + /** + * Loads properties from application.properties file. + */ + private void loadProperties() { + String[] possibleLocations = { + "/application.properties", + "application.properties", + "/config/application.properties" + }; + + boolean loaded = false; + for (String location : possibleLocations) { + try (InputStream inputStream = getClass().getResourceAsStream(location)) { + if (inputStream != null) { + properties.load(inputStream); + logger.info("Loaded properties from: " + location); + loaded = true; + break; + } + } catch (IOException e) { + logger.fine("Could not load properties from: " + location); + } + } + + if (!loaded) { + logger.info("No application.properties found, using default configuration"); + } + + logLoadedProperties(); + } + + /** + * Logs loaded properties (excluding sensitive data). + */ + private void logLoadedProperties() { + logger.info("Loaded " + properties.size() + " properties"); + properties.stringPropertyNames().stream() + .filter(key -> !key.toLowerCase().contains("password")) + .forEach(key -> logger.fine("Property: " + key + " = " + properties.getProperty(key))); + } + + /** + * Gets a string property with optional default value. + * + * @param key the property key + * @param defaultValue the default value if property is not found + * @return the property value or default value + */ + public String getProperty(String key, String defaultValue) { + return properties.getProperty(key, defaultValue); + } + + /** + * Gets a string property. + * + * @param key the property key + * @return the property value or null if not found + */ + public String getProperty(String key) { + return properties.getProperty(key); + } + + /** + * Gets a boolean property with default value. + * + * @param key the property key + * @param defaultValue the default value + * @return the boolean value + */ + public boolean getBooleanProperty(String key, boolean defaultValue) { + String value = properties.getProperty(key); + if (value == null) { + return defaultValue; + } + return Boolean.parseBoolean(value.trim()); + } + + /** + * Gets an integer property with default value. + * + * @param key the property key + * @param defaultValue the default value + * @return the integer value + */ + public int getIntProperty(String key, int defaultValue) { + String value = properties.getProperty(key); + if (value == null) { + return defaultValue; + } + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + logger.warning("Invalid integer value for property " + key + ": " + value + ", using default: " + defaultValue); + return defaultValue; + } + } + + /** + * Checks if a property exists. + * + * @param key the property key + * @return true if the property exists + */ + public boolean hasProperty(String key) { + return properties.containsKey(key); + } + + /** + * Gets all properties. + * + * @return the Properties object + */ + public Properties getAllProperties() { + return new Properties(properties); + } + + // Database-specific convenience methods + + /** + * Gets the database URL. + * + * @return the database URL or H2 default if not specified + */ + public String getDatabaseUrl() { + return getProperty("jazzy.datasource.url", "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); + } + + /** + * Gets the database username. + * + * @return the database username or "sa" if not specified + */ + public String getDatabaseUsername() { + return getProperty("jazzy.datasource.username", "sa"); + } + + /** + * Gets the database password. + * + * @return the database password or empty string if not specified + */ + public String getDatabasePassword() { + return getProperty("jazzy.datasource.password", ""); + } + + /** + * Gets the database driver class name. + * + * @return the driver class name or H2 driver if not specified + */ + public String getDatabaseDriverClassName() { + return getProperty("jazzy.datasource.driver-class-name", "org.h2.Driver"); + } + + /** + * Gets the Hibernate dialect. + * + * @return the Hibernate dialect or H2 dialect if not specified + */ + public String getHibernateDialect() { + return getProperty("jazzy.jpa.database-platform", "org.hibernate.dialect.H2Dialect"); + } + + /** + * Gets the Hibernate DDL auto mode. + * + * @return the DDL auto mode or "update" if not specified + */ + public String getHibernateDdlAuto() { + return getProperty("jazzy.jpa.hibernate.ddl-auto", "update"); + } + + /** + * Checks if SQL logging is enabled. + * + * @return true if SQL logging is enabled + */ + public boolean isShowSql() { + return getBooleanProperty("jazzy.jpa.show-sql", false); + } + + /** + * Checks if SQL formatting is enabled. + * + * @return true if SQL formatting is enabled + */ + public boolean isFormatSql() { + return getBooleanProperty("jazzy.jpa.properties.hibernate.format_sql", false); + } + + /** + * Checks if H2 console is enabled. + * + * @return true if H2 console is enabled + */ + public boolean isH2ConsoleEnabled() { + return getBooleanProperty("jazzy.h2.console.enabled", true); + } + + /** + * Checks if database is enabled. + * + * @return true if database is enabled + */ + public boolean isDatabaseEnabled() { + return getBooleanProperty("jazzy.database.enabled", true); + } +} \ No newline at end of file diff --git a/src/main/java/jazzyframework/core/RequestHandler.java b/src/main/java/jazzyframework/core/RequestHandler.java index 4bf5f33..c84dd34 100644 --- a/src/main/java/jazzyframework/core/RequestHandler.java +++ b/src/main/java/jazzyframework/core/RequestHandler.java @@ -220,23 +220,32 @@ public void run() { } else { out.write(Response.json(result).toHttpResponse()); } + + out.flush(); + Metrics.successfulRequests.incrementAndGet(); + logger.info("Response sent successfully."); + long duration = System.currentTimeMillis() - startTime; + Metrics.totalResponseTime.addAndGet(duration); + } catch (IllegalArgumentException e) { logger.warning("Bad request: " + e.getMessage()); out.write(ErrorResponse.badRequest(e.getMessage()).toHttpResponse()); + out.flush(); + } catch (Exception e) { + logger.severe("Error executing controller method: " + e.getMessage()); + e.printStackTrace(); + out.write(ErrorResponse.serverError("Internal server error: " + e.getMessage()).toHttpResponse()); + out.flush(); + Metrics.failedRequests.incrementAndGet(); } - out.flush(); - Metrics.successfulRequests.incrementAndGet(); - logger.info("Response sent successfully."); - long duration = System.currentTimeMillis() - startTime; - Metrics.totalResponseTime.addAndGet(duration); } catch (Exception e) { Metrics.failedRequests.incrementAndGet(); logger.severe("Exception handling request: " + e.getMessage()); e.printStackTrace(); try { BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream())); - out.write(ErrorResponse.serverError(e.getMessage()).toHttpResponse()); + out.write(ErrorResponse.serverError("Request processing failed: " + e.getMessage()).toHttpResponse()); out.flush(); } catch (Exception ex) { logger.severe("Error sending error response: " + ex.getMessage()); diff --git a/src/main/java/jazzyframework/data/BaseRepository.java b/src/main/java/jazzyframework/data/BaseRepository.java new file mode 100644 index 0000000..ea12924 --- /dev/null +++ b/src/main/java/jazzyframework/data/BaseRepository.java @@ -0,0 +1,156 @@ +package jazzyframework.data; + +import java.util.List; +import java.util.Optional; + +/** + * Base repository interface providing common CRUD operations. + * + *

This interface is inspired by Spring Data JPA's JpaRepository and provides + * a similar set of basic operations for entities. All repository interfaces + * should extend this interface to get automatic implementation of common operations. + * + *

Example usage: + *

+ * public interface UserRepository extends BaseRepository<User, Long> {
+ *     // Custom query methods can be added here
+ *     Optional<User> findByEmail(String email);
+ *     List<User> findByActiveTrue();
+ * }
+ * 
+ * + *

The generic parameters are: + *

    + *
  • {@code T} - The entity type
  • + *
  • {@code ID} - The type of the entity's primary key
  • + *
+ * + * @param the entity type + * @param the type of the entity's primary key + * + * @since 0.3.0 + * @author Caner Mastan + */ +public interface BaseRepository { + + /** + * Saves a given entity. + * + * @param entity the entity to save; must not be null + * @return the saved entity; will never be null + * @throws IllegalArgumentException if entity is null + */ + T save(T entity); + + /** + * Saves all given entities. + * + * @param entities the entities to save; must not be null and must not contain null elements + * @return the saved entities; will never be null + * @throws IllegalArgumentException if entities is null or contains null elements + */ + List saveAll(Iterable entities); + + /** + * Retrieves an entity by its id. + * + * @param id the id of the entity to retrieve; must not be null + * @return the entity with the given id or Optional#empty() if none found + * @throws IllegalArgumentException if id is null + */ + Optional findById(ID id); + + /** + * Returns whether an entity with the given id exists. + * + * @param id the id of the entity; must not be null + * @return true if an entity with the given id exists, false otherwise + * @throws IllegalArgumentException if id is null + */ + boolean existsById(ID id); + + /** + * Returns all instances of the type. + * + * @return all entities + */ + List findAll(); + + /** + * Returns all instances of the type with the given IDs. + * + * @param ids the IDs of the entities to retrieve; must not be null + * @return the entities with the given IDs + * @throws IllegalArgumentException if ids is null + */ + List findAllById(Iterable ids); + + /** + * Returns the number of entities available. + * + * @return the number of entities + */ + long count(); + + /** + * Deletes the entity with the given id. + * + * @param id the id of the entity to delete; must not be null + * @throws IllegalArgumentException if id is null + */ + void deleteById(ID id); + + /** + * Deletes a given entity. + * + * @param entity the entity to delete; must not be null + * @throws IllegalArgumentException if entity is null + */ + void delete(T entity); + + /** + * Deletes all instances of the type with the given IDs. + * + * @param ids the IDs of the entities to delete; must not be null + * @throws IllegalArgumentException if ids is null + */ + void deleteAllById(Iterable ids); + + /** + * Deletes the given entities. + * + * @param entities the entities to delete; must not be null + * @throws IllegalArgumentException if entities is null + */ + void deleteAll(Iterable entities); + + /** + * Deletes all entities managed by the repository. + */ + void deleteAll(); + + /** + * Flushes all pending changes to the database. + */ + void flush(); + + /** + * Saves an entity and flushes changes instantly. + * + * @param entity the entity to save + * @return the saved entity + */ + T saveAndFlush(T entity); + + /** + * Deletes the given entities in a batch which means it will create a single Query. + * + * @param entities the entities to delete + */ + void deleteInBatch(Iterable entities); + + /** + * Deletes all entities in a batch call. + */ + void deleteAllInBatch(); +} \ No newline at end of file diff --git a/src/main/java/jazzyframework/data/BaseRepositoryImpl.java b/src/main/java/jazzyframework/data/BaseRepositoryImpl.java new file mode 100644 index 0000000..a0d0d74 --- /dev/null +++ b/src/main/java/jazzyframework/data/BaseRepositoryImpl.java @@ -0,0 +1,389 @@ +package jazzyframework.data; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; +import org.hibernate.query.Query; + +import jakarta.persistence.Id; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * Default implementation of BaseRepository using Hibernate SessionFactory. + * + *

This class provides concrete implementations for all CRUD operations + * defined in BaseRepository interface. It uses Hibernate Session for + * database operations and handles transactions automatically. + * + *

Key features: + *

    + *
  • Automatic transaction management
  • + *
  • Generic type resolution for entity operations
  • + *
  • Efficient batch operations
  • + *
  • Proper exception handling and logging
  • + *
+ * + * @param the entity type + * @param the type of the entity's primary key + * + * @since 0.3.0 + * @author Caner Mastan + */ +public class BaseRepositoryImpl implements BaseRepository { + private static final Logger logger = Logger.getLogger(BaseRepositoryImpl.class.getName()); + + protected final SessionFactory sessionFactory; + protected final Class entityClass; + protected final Class idClass; + protected final String entityName; + protected final String idFieldName; + + /** + * Creates a new BaseRepositoryImpl with the given SessionFactory and entity types. + * + * @param sessionFactory the Hibernate SessionFactory + * @param entityClass the entity class + * @param idClass the ID class + */ + public BaseRepositoryImpl(SessionFactory sessionFactory, Class entityClass, Class idClass) { + this.sessionFactory = sessionFactory; + this.entityClass = entityClass; + this.idClass = idClass; + this.entityName = entityClass.getSimpleName(); + this.idFieldName = findIdFieldName(); + } + + /** + * Creates a new BaseRepositoryImpl with automatic type resolution. + * This constructor is used when the repository is created through reflection. + * + * @param sessionFactory the Hibernate SessionFactory + */ + @SuppressWarnings("unchecked") + public BaseRepositoryImpl(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + + // Resolve generic types from the class hierarchy + Type[] actualTypeArguments = getActualTypeArguments(); + this.entityClass = (Class) actualTypeArguments[0]; + this.idClass = (Class) actualTypeArguments[1]; + this.entityName = entityClass.getSimpleName(); + this.idFieldName = findIdFieldName(); + } + + /** + * Gets the actual type arguments for the generic types. + */ + private Type[] getActualTypeArguments() { + Type genericSuperclass = getClass().getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; + return parameterizedType.getActualTypeArguments(); + } + throw new IllegalStateException("Cannot resolve generic types for " + getClass()); + } + + /** + * Finds the ID field name using @Id annotation. + */ + private String findIdFieldName() { + Field[] fields = entityClass.getDeclaredFields(); + for (Field field : fields) { + if (field.isAnnotationPresent(Id.class)) { + return field.getName(); + } + } + return "id"; // fallback to default + } + + @Override + public T save(T entity) { + if (entity == null) { + throw new IllegalArgumentException("Entity must not be null"); + } + + try (Session session = sessionFactory.openSession()) { + Transaction transaction = session.beginTransaction(); + T savedEntity = session.merge(entity); + transaction.commit(); + return savedEntity; + } catch (Exception e) { + logger.severe("Error saving entity: " + e.getMessage()); + throw new RuntimeException("Failed to save entity", e); + } + } + + @Override + public List saveAll(Iterable entities) { + if (entities == null) { + throw new IllegalArgumentException("Entities must not be null"); + } + + List savedEntities = new ArrayList<>(); + + try (Session session = sessionFactory.openSession()) { + Transaction transaction = session.beginTransaction(); + + for (T entity : entities) { + if (entity == null) { + throw new IllegalArgumentException("Entity must not be null"); + } + T savedEntity = session.merge(entity); + savedEntities.add(savedEntity); + } + + transaction.commit(); + return savedEntities; + } catch (Exception e) { + logger.severe("Error saving entities: " + e.getMessage()); + throw new RuntimeException("Failed to save entities", e); + } + } + + @Override + public Optional findById(ID id) { + if (id == null) { + throw new IllegalArgumentException("ID must not be null"); + } + + try (Session session = sessionFactory.openSession()) { + Transaction transaction = session.beginTransaction(); + T entity = session.get(entityClass, id); + transaction.commit(); + return Optional.ofNullable(entity); + } catch (Exception e) { + logger.severe("Error finding entity by ID: " + e.getMessage()); + throw new RuntimeException("Failed to find entity", e); + } + } + + @Override + public boolean existsById(ID id) { + if (id == null) { + throw new IllegalArgumentException("ID must not be null"); + } + + try (Session session = sessionFactory.openSession()) { + Transaction transaction = session.beginTransaction(); + Query query = session.createQuery( + "SELECT COUNT(e) FROM " + entityName + " e WHERE e." + idFieldName + " = :id", Long.class); + query.setParameter("id", id); + Long count = query.uniqueResult(); + transaction.commit(); + return count != null && count > 0; + } catch (Exception e) { + logger.severe("Error checking entity existence: " + e.getMessage()); + throw new RuntimeException("Failed to check entity existence", e); + } + } + + @Override + public List findAll() { + try (Session session = sessionFactory.openSession()) { + Transaction transaction = session.beginTransaction(); + Query query = session.createQuery("FROM " + entityName, entityClass); + List entities = query.list(); + transaction.commit(); + return entities; + } catch (Exception e) { + logger.severe("Error finding all entities: " + e.getMessage()); + throw new RuntimeException("Failed to find entities", e); + } + } + + @Override + public List findAllById(Iterable ids) { + if (ids == null) { + throw new IllegalArgumentException("IDs must not be null"); + } + + List idList = new ArrayList<>(); + ids.forEach(idList::add); + + if (idList.isEmpty()) { + return new ArrayList<>(); + } + + try (Session session = sessionFactory.openSession()) { + Transaction transaction = session.beginTransaction(); + Query query = session.createQuery( + "FROM " + entityName + " e WHERE e." + idFieldName + " IN (:ids)", entityClass); + query.setParameterList("ids", idList); + List entities = query.list(); + transaction.commit(); + return entities; + } catch (Exception e) { + logger.severe("Error finding entities by IDs: " + e.getMessage()); + throw new RuntimeException("Failed to find entities", e); + } + } + + @Override + public long count() { + try (Session session = sessionFactory.openSession()) { + Transaction transaction = session.beginTransaction(); + Query query = session.createQuery("SELECT COUNT(e) FROM " + entityName + " e", Long.class); + Long count = query.uniqueResult(); + transaction.commit(); + return count != null ? count : 0L; + } catch (Exception e) { + logger.severe("Error counting entities: " + e.getMessage()); + throw new RuntimeException("Failed to count entities", e); + } + } + + @Override + public void deleteById(ID id) { + if (id == null) { + throw new IllegalArgumentException("ID must not be null"); + } + + try (Session session = sessionFactory.openSession()) { + Transaction transaction = session.beginTransaction(); + T entity = session.get(entityClass, id); + if (entity != null) { + session.remove(entity); + } + transaction.commit(); + logger.fine("Deleted entity with ID: " + id); + } catch (Exception e) { + logger.severe("Error deleting entity by ID: " + e.getMessage()); + throw new RuntimeException("Failed to delete entity", e); + } + } + + @Override + public void delete(T entity) { + if (entity == null) { + throw new IllegalArgumentException("Entity must not be null"); + } + + try (Session session = sessionFactory.openSession()) { + Transaction transaction = session.beginTransaction(); + session.remove(entity); + transaction.commit(); + logger.fine("Deleted entity: " + entityName); + } catch (Exception e) { + logger.severe("Error deleting entity: " + e.getMessage()); + throw new RuntimeException("Failed to delete entity", e); + } + } + + @Override + public void deleteAllById(Iterable ids) { + if (ids == null) { + throw new IllegalArgumentException("IDs must not be null"); + } + + List idList = new ArrayList<>(); + ids.forEach(idList::add); + + if (idList.isEmpty()) { + return; + } + + try (Session session = sessionFactory.openSession()) { + Transaction transaction = session.beginTransaction(); + Query query = session.createQuery( // FIXME: deprecated method + "DELETE FROM " + entityName + " e WHERE e." + idFieldName + " IN (:ids)"); + query.setParameterList("ids", idList); + int deletedCount = query.executeUpdate(); + transaction.commit(); + logger.fine("Deleted " + deletedCount + " entities by IDs"); + } catch (Exception e) { + logger.severe("Error deleting entities by IDs: " + e.getMessage()); + throw new RuntimeException("Failed to delete entities", e); + } + } + + @Override + public void deleteAll(Iterable entities) { + if (entities == null) { + throw new IllegalArgumentException("Entities must not be null"); + } + + try (Session session = sessionFactory.openSession()) { + Transaction transaction = session.beginTransaction(); + + for (T entity : entities) { + if (entity != null) { + session.remove(entity); + } + } + + transaction.commit(); + logger.fine("Deleted entities of type: " + entityName); + } catch (Exception e) { + logger.severe("Error deleting entities: " + e.getMessage()); + throw new RuntimeException("Failed to delete entities", e); + } + } + + @Override + public void deleteAll() { + try (Session session = sessionFactory.openSession()) { + Transaction transaction = session.beginTransaction(); + Query query = session.createQuery("DELETE FROM " + entityName); // FIXME: deprecated method + int deletedCount = query.executeUpdate(); + transaction.commit(); + logger.fine("Deleted all " + deletedCount + " entities of type: " + entityName); + } catch (Exception e) { + logger.severe("Error deleting all entities: " + e.getMessage()); + throw new RuntimeException("Failed to delete all entities", e); + } + } + + @Override + public void flush() { + try (Session session = sessionFactory.openSession()) { + session.flush(); + } catch (Exception e) { + logger.severe("Error flushing session: " + e.getMessage()); + throw new RuntimeException("Failed to flush session", e); + } + } + + @Override + public T saveAndFlush(T entity) { + T savedEntity = save(entity); + flush(); + return savedEntity; + } + + @Override + public void deleteInBatch(Iterable entities) { + if (entities == null) { + throw new IllegalArgumentException("Entities must not be null"); + } + + List ids = new ArrayList<>(); + for (T entity : entities) { + if (entity != null) { + try { + Field idField = entityClass.getDeclaredField(idFieldName); + idField.setAccessible(true); + @SuppressWarnings("unchecked") + ID id = (ID) idField.get(entity); + if (id != null) { + ids.add(id); + } + } catch (Exception e) { + logger.warning("Could not extract ID from entity: " + e.getMessage()); + } + } + } + + deleteAllById(ids); + } + + @Override + public void deleteAllInBatch() { + deleteAll(); + } +} \ No newline at end of file diff --git a/src/main/java/jazzyframework/data/EntityScanner.java b/src/main/java/jazzyframework/data/EntityScanner.java new file mode 100644 index 0000000..7d94cd9 --- /dev/null +++ b/src/main/java/jazzyframework/data/EntityScanner.java @@ -0,0 +1,217 @@ +package jazzyframework.data; + +import jakarta.persistence.Entity; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.logging.Logger; + +/** + * Scans packages for classes annotated with {@code @Entity} for Hibernate registration. + * + *

This scanner provides automatic entity discovery by: + *

    + *
  • Detecting the main class package from stack trace
  • + *
  • Recursively scanning all sub-packages
  • + *
  • Finding classes annotated with {@code @Entity}
  • + *
  • Providing the discovered entities to Hibernate configuration
  • + *
+ * + *

The scanning algorithm is identical to ComponentScanner but specifically + * looks for JPA entities instead of DI components. + * + * @since 0.3.0 + * @author Caner Mastan + */ +public class EntityScanner { + private static final Logger logger = Logger.getLogger(EntityScanner.class.getName()); + + /** + * Scans for entity classes starting from the main class package. + * + * @return list of entity classes found + */ + public List> scanForEntities() { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + String mainClassName = null; + + for (StackTraceElement element : stackTrace) { + if ("main".equals(element.getMethodName())) { + mainClassName = element.getClassName(); + break; + } + } + + if (mainClassName == null) { + logger.warning("Could not detect main class, scanning common packages for entities"); + return scanCommonPackagesForEntities(); + } + + String basePackage = getBasePackage(mainClassName); + logger.info("Scanning for entities from package: " + basePackage); + + return scanPackageForEntities(basePackage); + } + + /** + * Scans common package patterns when main class detection fails. + * + * @return list of entity classes found + */ + private List> scanCommonPackagesForEntities() { + List> allEntities = new ArrayList<>(); + String[] commonPackages = {"com", "org", "net", "examples"}; + + for (String pkg : commonPackages) { + try { + allEntities.addAll(scanPackageForEntities(pkg)); + } catch (Exception e) { + // Ignore packages that don't exist + } + } + + return allEntities; + } + + /** + * Gets the base package from a fully qualified class name. + * + * @param className the fully qualified class name + * @return the base package name + */ + private String getBasePackage(String className) { + int lastDot = className.lastIndexOf('.'); + if (lastDot > 0) { + return className.substring(0, lastDot); + } + return ""; + } + + /** + * Scans the specified package for entity classes. + * + * @param packageName the base package to scan + * @return list of entity classes found + */ + public List> scanPackageForEntities(String packageName) { + List> entityClasses = new ArrayList<>(); + + try { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + String path = packageName.replace('.', '/'); + Enumeration resources = classLoader.getResources(path); + + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + File file = new File(resource.getFile()); + + if (file.isDirectory()) { + entityClasses.addAll(findEntityClasses(file, packageName)); + } + } + } catch (IOException e) { + logger.severe("Error scanning package for entities: " + packageName + " - " + e.getMessage()); + } + + if (!entityClasses.isEmpty()) { + logger.fine("Found " + entityClasses.size() + " entity classes in package: " + packageName); + } + return entityClasses; + } + + /** + * Recursively finds entity classes in the given directory. + * + * @param directory the directory to search + * @param packageName the package name corresponding to the directory + * @return list of entity classes found + */ + private List> findEntityClasses(File directory, String packageName) { + List> entityClasses = new ArrayList<>(); + + if (!directory.exists()) { + return entityClasses; + } + + File[] files = directory.listFiles(); + if (files == null) { + return entityClasses; + } + + for (File file : files) { + if (file.isDirectory()) { + entityClasses.addAll(findEntityClasses(file, packageName + "." + file.getName())); + } else if (file.getName().endsWith(".class")) { + String className = packageName + '.' + file.getName().substring(0, file.getName().length() - 6); + try { + Class clazz = Class.forName(className); + if (isEntity(clazz)) { + entityClasses.add(clazz); + logger.fine("Found entity: " + className); + } + } catch (ClassNotFoundException e) { + logger.warning("Could not load class: " + className); + } catch (NoClassDefFoundError e) { + // Ignore classes that have missing dependencies + logger.fine("Skipping class with missing dependencies: " + className); + } + } + } + + return entityClasses; + } + + /** + * Checks if a class is annotated with {@code @Entity}. + * + * @param clazz the class to check + * @return true if the class has {@code @Entity} annotation + */ + private boolean isEntity(Class clazz) { + return clazz.isAnnotationPresent(Entity.class); + } + + /** + * Scans specific packages for entities. + * Useful for testing or when you want to limit the scanning scope. + * + * @param packages array of package names to scan + * @return list of entity classes found + */ + public List> scanPackagesForEntities(String... packages) { + List> allEntities = new ArrayList<>(); + + for (String packageName : packages) { + allEntities.addAll(scanPackageForEntities(packageName)); + } + + return allEntities; + } + + /** + * Validates that an entity class is properly configured. + * + * @param entityClass the entity class to validate + * @return true if the entity is valid, false otherwise + */ + public boolean validateEntity(Class entityClass) { + if (!isEntity(entityClass)) { + logger.warning("Class is not annotated with @Entity: " + entityClass.getName()); + return false; + } + + // Check for default constructor + try { + entityClass.getDeclaredConstructor(); + } catch (NoSuchMethodException e) { + logger.warning("Entity class does not have a default constructor: " + entityClass.getName()); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/main/java/jazzyframework/data/HibernateConfig.java b/src/main/java/jazzyframework/data/HibernateConfig.java new file mode 100644 index 0000000..ddcb3ec --- /dev/null +++ b/src/main/java/jazzyframework/data/HibernateConfig.java @@ -0,0 +1,241 @@ +package jazzyframework.data; + +import jazzyframework.core.PropertyLoader; +import jazzyframework.di.annotations.Component; +import jazzyframework.di.annotations.PostConstruct; +import jazzyframework.di.annotations.PreDestroy; + +import org.hibernate.SessionFactory; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.Configuration; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import javax.sql.DataSource; +import java.util.List; +import java.util.logging.Logger; + +/** + * Hibernate configuration and SessionFactory management. + * + *

This class provides zero-configuration Hibernate setup by: + *

    + *
  • Reading configuration from application.properties
  • + *
  • Automatically discovering @Entity classes
  • + *
  • Setting up HikariCP connection pool
  • + *
  • Creating and managing SessionFactory
  • + *
  • Providing proper cleanup on shutdown
  • + *
+ * + *

Supports multiple databases out of the box: + *

    + *
  • H2 (embedded, default for development)
  • + *
  • MySQL (production ready)
  • + *
  • PostgreSQL (production ready)
  • + *
+ * + *

Configuration properties (all prefixed with "jazzy"): + *

    + *
  • jazzy.datasource.* - Database connection settings
  • + *
  • jazzy.jpa.* - JPA/Hibernate settings
  • + *
  • jazzy.h2.console.enabled - H2 console enablement
  • + *
+ * + * @since 0.3.0 + * @author Caner Mastan + */ +@Component +public class HibernateConfig { + private static final Logger logger = Logger.getLogger(HibernateConfig.class.getName()); + + private SessionFactory sessionFactory; + private HikariDataSource dataSource; + private final PropertyLoader propertyLoader; + private final EntityScanner entityScanner; + + /** + * Creates a new HibernateConfig instance. + * Dependencies will be injected by the DI container. + */ + public HibernateConfig() { + this.propertyLoader = PropertyLoader.getInstance(); + this.entityScanner = new EntityScanner(); + } + + /** + * Initializes Hibernate configuration after bean creation. + * This method is called automatically by the DI container. + */ + @PostConstruct + public void initialize() { + if (!propertyLoader.isDatabaseEnabled()) { + logger.info("Database is disabled via configuration"); + return; + } + + logger.info("Initializing Hibernate configuration..."); + + try { + createDataSource(); + createSessionFactory(); + startH2ConsoleIfEnabled(); + + logger.info("Hibernate initialized successfully"); + } catch (Exception e) { + logger.severe("Failed to initialize Hibernate: " + e.getMessage()); + throw new RuntimeException("Hibernate initialization failed", e); + } + } + + /** + * Creates HikariCP data source based on properties. + */ + private void createDataSource() { + HikariConfig config = new HikariConfig(); + + config.setJdbcUrl(propertyLoader.getDatabaseUrl()); + config.setUsername(propertyLoader.getDatabaseUsername()); + config.setPassword(propertyLoader.getDatabasePassword()); + config.setDriverClassName(propertyLoader.getDatabaseDriverClassName()); + + config.setMaximumPoolSize(propertyLoader.getIntProperty("jazzy.datasource.hikari.maximum-pool-size", 10)); + config.setMinimumIdle(propertyLoader.getIntProperty("jazzy.datasource.hikari.minimum-idle", 2)); + config.setConnectionTimeout(propertyLoader.getIntProperty("jazzy.datasource.hikari.connection-timeout", 30000)); + config.setIdleTimeout(propertyLoader.getIntProperty("jazzy.datasource.hikari.idle-timeout", 600000)); + config.setMaxLifetime(propertyLoader.getIntProperty("jazzy.datasource.hikari.max-lifetime", 1800000)); + + config.setConnectionTestQuery("SELECT 1"); + config.setValidationTimeout(5000); + + this.dataSource = new HikariDataSource(config); + + logger.info("DataSource created: " + config.getJdbcUrl()); + } + + /** + * Creates Hibernate SessionFactory with automatic entity discovery. + */ + private void createSessionFactory() { + Configuration configuration = new Configuration(); + + configuration.setProperty("hibernate.dialect", propertyLoader.getHibernateDialect()); + configuration.setProperty("hibernate.hbm2ddl.auto", propertyLoader.getHibernateDdlAuto()); + configuration.setProperty("hibernate.show_sql", String.valueOf(propertyLoader.isShowSql())); + configuration.setProperty("hibernate.format_sql", String.valueOf(propertyLoader.isFormatSql())); + + configuration.setProperty("hibernate.use_sql_comments", "true"); + configuration.setProperty("hibernate.jdbc.batch_size", "20"); + configuration.setProperty("hibernate.order_inserts", "true"); + configuration.setProperty("hibernate.order_updates", "true"); + configuration.setProperty("hibernate.jdbc.batch_versioned_data", "true"); + + addCustomHibernateProperties(configuration); + + List> entityClasses = entityScanner.scanForEntities(); + if (!entityClasses.isEmpty()) { + logger.info("Found " + entityClasses.size() + " entity classes"); + } else { + logger.warning("No entity classes found"); + } + + for (Class entityClass : entityClasses) { + configuration.addAnnotatedClass(entityClass); + logger.fine("Registered entity: " + entityClass.getName()); + } + + StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder() + .applySettings(configuration.getProperties()) + .applySetting("hibernate.connection.datasource", dataSource) + .build(); + + this.sessionFactory = configuration.buildSessionFactory(serviceRegistry); + + logger.info("SessionFactory created successfully"); + } + + /** + * Adds custom Hibernate properties from application.properties. + */ + private void addCustomHibernateProperties(Configuration configuration) { + propertyLoader.getAllProperties().stringPropertyNames().stream() + .filter(key -> key.startsWith("jazzy.jpa.properties.hibernate.")) + .forEach(key -> { + String hibernateKey = key.substring("jazzy.jpa.properties.".length()); + String value = propertyLoader.getProperty(key); + configuration.setProperty(hibernateKey, value); + logger.fine("Added custom Hibernate property: " + hibernateKey + " = " + value); + }); + } + + /** + * Starts H2 console if enabled and using H2 database. + */ + private void startH2ConsoleIfEnabled() { + if (propertyLoader.isH2ConsoleEnabled() && + propertyLoader.getDatabaseUrl().contains("h2")) { + + try { + org.h2.tools.Server.createTcpServer("-tcpAllowOthers").start(); + org.h2.tools.Server.createWebServer("-webAllowOthers", "-webPort", "8082").start(); + + logger.info("H2 Console started at: http://localhost:8082"); + logger.info("H2 Console URL: " + propertyLoader.getDatabaseUrl()); + } catch (Exception e) { + logger.warning("Could not start H2 console: " + e.getMessage()); + } + } + } + + /** + * Cleanup resources when the application shuts down. + */ + @PreDestroy + public void destroy() { + logger.info("Shutting down Hibernate..."); + + if (sessionFactory != null && !sessionFactory.isClosed()) { + sessionFactory.close(); + logger.info("SessionFactory closed"); + } + + if (dataSource != null && !dataSource.isClosed()) { + dataSource.close(); + logger.info("DataSource closed"); + } + } + + /** + * Gets the Hibernate SessionFactory. + * + * @return the SessionFactory instance + */ + public SessionFactory getSessionFactory() { + if (sessionFactory == null) { + throw new IllegalStateException("SessionFactory is not initialized. Ensure database is enabled."); + } + return sessionFactory; + } + + /** + * Gets the HikariCP DataSource. + * + * @return the DataSource instance + */ + public DataSource getDataSource() { + if (dataSource == null) { + throw new IllegalStateException("DataSource is not initialized. Ensure database is enabled."); + } + return dataSource; + } + + /** + * Checks if Hibernate is properly initialized. + * + * @return true if SessionFactory is available + */ + public boolean isInitialized() { + return sessionFactory != null && !sessionFactory.isClosed(); + } +} \ No newline at end of file diff --git a/src/main/java/jazzyframework/data/QueryMethodParser.java b/src/main/java/jazzyframework/data/QueryMethodParser.java new file mode 100644 index 0000000..1cc43e1 --- /dev/null +++ b/src/main/java/jazzyframework/data/QueryMethodParser.java @@ -0,0 +1,260 @@ +package jazzyframework.data; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parses repository method names and generates corresponding HQL queries. + * + *

This class implements Spring Data JPA-like method name parsing to automatically + * generate queries from method names. Supported patterns include: + * + *

    + *
  • findBy* - SELECT queries
  • + *
  • countBy* - COUNT queries
  • + *
  • existsBy* - EXISTS queries
  • + *
  • deleteBy* - DELETE queries
  • + *
+ * + *

Supported keywords: + *

    + *
  • And, Or - logical operators
  • + *
  • GreaterThan, LessThan, GreaterThanEqual, LessThanEqual - comparison
  • + *
  • Like, NotLike, Containing, StartingWith, EndingWith - string matching
  • + *
  • IsNull, IsNotNull - null checks
  • + *
  • In, NotIn - collection membership
  • + *
  • True, False - boolean values
  • + *
+ * + * @since 0.3.0 + * @author Caner Mastan + */ +public class QueryMethodParser { + + private static final Pattern METHOD_PATTERN = Pattern.compile( + "^(find|count|exists|delete)By(.+)$" + ); + + private static final String[] KEYWORDS = { + "And", "Or", "GreaterThan", "LessThan", "GreaterThanEqual", "LessThanEqual", + "Like", "NotLike", "Containing", "StartingWith", "EndingWith", + "IsNull", "IsNotNull", "In", "NotIn", "True", "False", "Between" + }; + + /** + * Checks if a method name can be parsed into a query. + * + * @param methodName the method name to check + * @return true if the method name follows a parseable pattern + */ + public boolean canParseMethodName(String methodName) { + return METHOD_PATTERN.matcher(methodName).matches(); + } + + /** + * Parses a method and generates the corresponding HQL query. + * + * @param method the method to parse + * @param entityClass the entity class for the repository + * @return the generated query information + */ + public QueryInfo parseMethod(Method method, Class entityClass) { + String methodName = method.getName(); + Matcher matcher = METHOD_PATTERN.matcher(methodName); + + if (!matcher.matches()) { + throw new IllegalArgumentException("Cannot parse method name: " + methodName); + } + + String operation = matcher.group(1); + String criteria = matcher.group(2); + + String entityName = entityClass.getSimpleName(); + String alias = entityName.toLowerCase().charAt(0) + ""; + + QueryInfo queryInfo = new QueryInfo(); + queryInfo.setEntityClass(entityClass); + queryInfo.setOperation(operation); + + // Parse criteria and build query + List conditions = new ArrayList<>(); + List parameterNames = new ArrayList<>(); + + parseCriteria(criteria, conditions, parameterNames, alias); + + // Build the query based on operation + String query = buildQuery(operation, entityName, alias, conditions); + queryInfo.setQuery(query); + queryInfo.setParameterNames(parameterNames); + + // Determine return type + Class returnType = method.getReturnType(); + if (returnType == Optional.class) { + Type genericType = ((ParameterizedType) method.getGenericReturnType()).getActualTypeArguments()[0]; + queryInfo.setReturnType((Class) genericType); + queryInfo.setOptionalReturn(true); + } else { + queryInfo.setReturnType(returnType); + } + + return queryInfo; + } + + /** + * Parses the criteria part of the method name. + */ + private void parseCriteria(String criteria, List conditions, List parameterNames, String alias) { + String remaining = criteria; + + while (!remaining.isEmpty()) { + boolean found = false; + + // Try to match keywords + for (String keyword : KEYWORDS) { + if (remaining.startsWith(keyword)) { + String beforeKeyword = remaining.substring(0, remaining.indexOf(keyword)); + if (!beforeKeyword.isEmpty()) { + String fieldName = camelToSnake(beforeKeyword); + String condition = buildCondition(fieldName, keyword, alias, parameterNames); + conditions.add(condition); + } + remaining = remaining.substring(keyword.length()); + found = true; + break; + } + } + + if (!found) { + // No keyword found, treat as simple field equality + String fieldName = camelToSnake(remaining); + String paramName = fieldName.replace(".", "_"); + conditions.add(alias + "." + fieldName + " = :" + paramName); + parameterNames.add(paramName); + break; + } + } + } + + /** + * Builds a condition based on the field name and keyword. + */ + private String buildCondition(String fieldName, String keyword, String alias, List parameterNames) { + String paramName = fieldName.replace(".", "_"); + String field = alias + "." + fieldName; + + switch (keyword) { + case "GreaterThan": + parameterNames.add(paramName); + return field + " > :" + paramName; + case "LessThan": + parameterNames.add(paramName); + return field + " < :" + paramName; + case "GreaterThanEqual": + parameterNames.add(paramName); + return field + " >= :" + paramName; + case "LessThanEqual": + parameterNames.add(paramName); + return field + " <= :" + paramName; + case "Like": + parameterNames.add(paramName); + return field + " LIKE :" + paramName; + case "NotLike": + parameterNames.add(paramName); + return field + " NOT LIKE :" + paramName; + case "Containing": + parameterNames.add(paramName); + return "LOWER(" + field + ") LIKE LOWER(:" + paramName + ")"; + case "StartingWith": + parameterNames.add(paramName); + return "LOWER(" + field + ") LIKE LOWER(:" + paramName + ")"; + case "EndingWith": + parameterNames.add(paramName); + return "LOWER(" + field + ") LIKE LOWER(:" + paramName + ")"; + case "IsNull": + return field + " IS NULL"; + case "IsNotNull": + return field + " IS NOT NULL"; + case "In": + parameterNames.add(paramName); + return field + " IN (:" + paramName + ")"; + case "NotIn": + parameterNames.add(paramName); + return field + " NOT IN (:" + paramName + ")"; + case "True": + return field + " = true"; + case "False": + return field + " = false"; + case "Between": + parameterNames.add(paramName + "Start"); + parameterNames.add(paramName + "End"); + return field + " BETWEEN :" + paramName + "Start AND :" + paramName + "End"; + default: + parameterNames.add(paramName); + return field + " = :" + paramName; + } + } + + /** + * Builds the complete query based on operation and conditions. + */ + private String buildQuery(String operation, String entityName, String alias, List conditions) { + String whereClause = conditions.isEmpty() ? "" : " WHERE " + String.join(" AND ", conditions); + + switch (operation) { + case "find": + return "SELECT " + alias + " FROM " + entityName + " " + alias + whereClause; + case "count": + return "SELECT COUNT(" + alias + ") FROM " + entityName + " " + alias + whereClause; + case "exists": + return "SELECT COUNT(" + alias + ") FROM " + entityName + " " + alias + whereClause; + case "delete": + return "DELETE FROM " + entityName + " " + alias + whereClause; + default: + throw new IllegalArgumentException("Unsupported operation: " + operation); + } + } + + /** + * Converts camelCase to snake_case for field names. + */ + private String camelToSnake(String camelCase) { + return camelCase.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase(); + } + + /** + * Information about a parsed query method. + */ + public static class QueryInfo { + private String query; + private List parameterNames = new ArrayList<>(); + private Class returnType; + private Class entityClass; + private String operation; + private boolean optionalReturn = false; + + // Getters and setters + public String getQuery() { return query; } + public void setQuery(String query) { this.query = query; } + + public List getParameterNames() { return parameterNames; } + public void setParameterNames(List parameterNames) { this.parameterNames = parameterNames; } + + public Class getReturnType() { return returnType; } + public void setReturnType(Class returnType) { this.returnType = returnType; } + + public Class getEntityClass() { return entityClass; } + public void setEntityClass(Class entityClass) { this.entityClass = entityClass; } + + public String getOperation() { return operation; } + public void setOperation(String operation) { this.operation = operation; } + + public boolean isOptionalReturn() { return optionalReturn; } + public void setOptionalReturn(boolean optionalReturn) { this.optionalReturn = optionalReturn; } + } +} \ No newline at end of file diff --git a/src/main/java/jazzyframework/data/RepositoryFactory.java b/src/main/java/jazzyframework/data/RepositoryFactory.java new file mode 100644 index 0000000..ac7b06b --- /dev/null +++ b/src/main/java/jazzyframework/data/RepositoryFactory.java @@ -0,0 +1,348 @@ +package jazzyframework.data; + +import jazzyframework.data.annotations.Modifying; +import jazzyframework.data.annotations.Query; +import jazzyframework.di.annotations.Component; +import jazzyframework.di.annotations.PostConstruct; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +/** + * Factory for creating repository implementations automatically. + * + *

This factory creates proxy implementations for repository interfaces + * that extend BaseRepository, similar to Spring Data JPA's approach. + * The proxy supports: + *

    + *
  • Method name parsing (findByEmail, countByActive, etc.)
  • + *
  • @Query annotations for custom HQL/JPQL
  • + *
  • Native SQL queries
  • + *
  • @Modifying annotations for UPDATE/DELETE operations
  • + *
+ * + * @since 0.3.0 + * @author Caner Mastan + */ +@Component +public class RepositoryFactory { + private static final Logger logger = Logger.getLogger(RepositoryFactory.class.getName()); + + private final HibernateConfig hibernateConfig; + private final ConcurrentHashMap, Object> repositoryCache = new ConcurrentHashMap<>(); + private final QueryMethodParser queryParser = new QueryMethodParser(); + private SessionFactory sessionFactory; + + /** + * Creates a new RepositoryFactory. + * HibernateConfig will be injected by the DI container. + */ + public RepositoryFactory(HibernateConfig hibernateConfig) { + this.hibernateConfig = hibernateConfig; + } + + /** + * Initializes the factory after DI container setup. + */ + @PostConstruct + public void initialize() { + if (hibernateConfig.isInitialized()) { + this.sessionFactory = hibernateConfig.getSessionFactory(); + logger.info("RepositoryFactory initialized with SessionFactory"); + } else { + logger.warning("HibernateConfig is not initialized, repositories may not work properly"); + } + } + + /** + * Creates a repository implementation for the given repository interface. + * + * @param repositoryInterface the repository interface class + * @param the repository type + * @return the repository implementation + */ + @SuppressWarnings("unchecked") + public T createRepository(Class repositoryInterface) { + if (sessionFactory == null) { + throw new IllegalStateException("SessionFactory is not available. Ensure database is enabled and configured."); + } + + // Check cache first + T cachedRepository = (T) repositoryCache.get(repositoryInterface); + if (cachedRepository != null) { + return cachedRepository; + } + + // Validate that the interface extends BaseRepository + if (!BaseRepository.class.isAssignableFrom(repositoryInterface)) { + throw new IllegalArgumentException("Repository interface must extend BaseRepository: " + repositoryInterface.getName()); + } + + // Extract generic type parameters + Type[] genericTypes = extractGenericTypes(repositoryInterface); + Class entityClass = (Class) genericTypes[0]; + Class idClass = (Class) genericTypes[1]; + + // Create the implementation + BaseRepositoryImpl implementation = new BaseRepositoryImpl<>(sessionFactory, entityClass, idClass); + + // Create proxy with enhanced handler + T proxy = (T) Proxy.newProxyInstance( + repositoryInterface.getClassLoader(), + new Class[]{repositoryInterface}, + new EnhancedRepositoryInvocationHandler(implementation, entityClass, sessionFactory, queryParser) + ); + + // Cache the proxy + repositoryCache.put(repositoryInterface, proxy); + + logger.fine("Created repository: " + repositoryInterface.getSimpleName() + + " for entity: " + entityClass.getSimpleName()); + + return proxy; + } + + /** + * Extracts generic type parameters from repository interface. + * + * @param repositoryInterface the repository interface + * @return array of generic types [entityType, idType] + */ + private Type[] extractGenericTypes(Class repositoryInterface) { + // Look for BaseRepository in the interface hierarchy + Type[] genericInterfaces = repositoryInterface.getGenericInterfaces(); + + for (Type genericInterface : genericInterfaces) { + if (genericInterface instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericInterface; + Type rawType = parameterizedType.getRawType(); + + if (BaseRepository.class.equals(rawType)) { + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + if (actualTypeArguments.length == 2) { + return actualTypeArguments; + } + } + } + } + + throw new IllegalArgumentException("Could not extract generic types from repository interface: " + repositoryInterface.getName()); + } + + /** + * Checks if a repository has been created for the given interface. + * + * @param repositoryInterface the repository interface + * @return true if a repository exists in cache + */ + public boolean hasRepository(Class repositoryInterface) { + return repositoryCache.containsKey(repositoryInterface); + } + + /** + * Clears the repository cache. + */ + public void clearCache() { + repositoryCache.clear(); + } + + /** + * Gets the number of cached repositories. + * + * @return the cache size + */ + public int getCacheSize() { + return repositoryCache.size(); + } + + /** + * Enhanced InvocationHandler for repository proxies with query support. + */ + private static class EnhancedRepositoryInvocationHandler implements InvocationHandler { + private final BaseRepositoryImpl implementation; + private final Class entityClass; + private final SessionFactory sessionFactory; + private final QueryMethodParser queryParser; + + public EnhancedRepositoryInvocationHandler(BaseRepositoryImpl implementation, + Class entityClass, + SessionFactory sessionFactory, + QueryMethodParser queryParser) { + this.implementation = implementation; + this.entityClass = entityClass; + this.sessionFactory = sessionFactory; + this.queryParser = queryParser; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + // Handle Object methods + if (method.getDeclaringClass() == Object.class) { + return method.invoke(implementation, args); + } + + // Handle toString specially for better debugging + if ("toString".equals(method.getName()) && method.getParameterCount() == 0) { + return "RepositoryProxy[" + entityClass.getSimpleName() + "Repository]"; + } + + // 1. Check for @Query annotation + Query queryAnnotation = method.getAnnotation(Query.class); + if (queryAnnotation != null) { + return executeCustomQuery(method, args, queryAnnotation); + } + + // 2. Check if method name can be parsed + if (queryParser.canParseMethodName(method.getName())) { + return executeGeneratedQuery(method, args); + } + + // 3. Handle default methods + if (method.isDefault()) { + return java.lang.invoke.MethodHandles.lookup() + .findSpecial( + method.getDeclaringClass(), + method.getName(), + java.lang.invoke.MethodType.methodType(method.getReturnType(), method.getParameterTypes()), + method.getDeclaringClass() + ) + .bindTo(proxy) + .invokeWithArguments(args); + } + + // 4. Delegate to BaseRepository implementation + return method.invoke(implementation, args); + + } catch (java.lang.reflect.InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Error) { + throw (Error) cause; + } else { + throw new RuntimeException("Repository operation failed", cause); + } + } catch (Exception e) { + throw new RuntimeException("Unexpected repository error", e); + } + } + + /** + * Executes a custom query defined by @Query annotation. + */ + private Object executeCustomQuery(Method method, Object[] args, Query queryAnnotation) { + String queryString = queryAnnotation.value(); + boolean isNative = queryAnnotation.nativeQuery(); + boolean isModifying = method.isAnnotationPresent(Modifying.class); + + try (Session session = sessionFactory.openSession()) { + Transaction transaction = session.beginTransaction(); + + org.hibernate.query.Query query; + if (isNative) { + query = session.createNativeQuery(queryString); + } else { + query = session.createQuery(queryString); + } + + // Set parameters + if (args != null) { + for (int i = 0; i < args.length; i++) { + query.setParameter(i + 1, args[i]); + } + } + + Object result; + if (isModifying) { + result = query.executeUpdate(); + } else { + Class returnType = method.getReturnType(); + if (returnType == Optional.class) { + result = Optional.ofNullable(query.uniqueResult()); + } else if (List.class.isAssignableFrom(returnType)) { + result = query.list(); + } else if (returnType == Long.class || returnType == long.class) { + result = query.uniqueResult(); + } else { + result = query.uniqueResult(); + } + } + + transaction.commit(); + return result; + } + } + + /** + * Executes a query generated from method name parsing. + */ + private Object executeGeneratedQuery(Method method, Object[] args) { + QueryMethodParser.QueryInfo queryInfo = queryParser.parseMethod(method, entityClass); + + try (Session session = sessionFactory.openSession()) { + Transaction transaction = session.beginTransaction(); + + org.hibernate.query.Query query = session.createQuery(queryInfo.getQuery()); + + // Set parameters + List paramNames = queryInfo.getParameterNames(); + if (args != null && paramNames.size() <= args.length) { + for (int i = 0; i < paramNames.size(); i++) { + Object value = args[i]; + + // Handle special cases for string operations + String paramName = paramNames.get(i); + if (queryInfo.getQuery().contains("LIKE") && value instanceof String) { + String stringValue = (String) value; + if (queryInfo.getQuery().contains("Containing")) { + value = "%" + stringValue + "%"; + } else if (queryInfo.getQuery().contains("StartingWith")) { + value = stringValue + "%"; + } else if (queryInfo.getQuery().contains("EndingWith")) { + value = "%" + stringValue; + } + } + + query.setParameter(paramName, value); + } + } + + Object result; + String operation = queryInfo.getOperation(); + + if ("exists".equals(operation)) { + Long count = (Long) query.uniqueResult(); + result = count != null && count > 0; + } else if ("count".equals(operation)) { + result = query.uniqueResult(); + } else if ("delete".equals(operation)) { + result = query.executeUpdate(); + } else { + // find operation + Class returnType = method.getReturnType(); + if (returnType == Optional.class || queryInfo.isOptionalReturn()) { + result = Optional.ofNullable(query.uniqueResult()); + } else if (List.class.isAssignableFrom(returnType)) { + result = query.list(); + } else { + result = query.uniqueResult(); + } + } + + transaction.commit(); + return result; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/jazzyframework/data/RepositoryScanner.java b/src/main/java/jazzyframework/data/RepositoryScanner.java new file mode 100644 index 0000000..3c61127 --- /dev/null +++ b/src/main/java/jazzyframework/data/RepositoryScanner.java @@ -0,0 +1,269 @@ +package jazzyframework.data; + +import jazzyframework.di.BeanDefinition; +import jazzyframework.di.annotations.Component; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.logging.Logger; + +/** + * Scans packages for repository interfaces and creates proxy implementations. + * + *

This scanner automatically discovers interfaces that extend BaseRepository + * and creates proxy implementations using RepositoryFactory. The proxies are + * then registered with the DI container for automatic injection. + * + *

Key features: + *

    + *
  • Automatic repository interface discovery
  • + *
  • Proxy implementation creation
  • + *
  • DI container integration
  • + *
  • Type-safe generic handling
  • + *
+ * + * @since 0.3.0 + * @author Caner Mastan + */ +@Component +public class RepositoryScanner { + private static final Logger logger = Logger.getLogger(RepositoryScanner.class.getName()); + + private final RepositoryFactory repositoryFactory; + private final List> discoveredRepositories = new ArrayList<>(); + + /** + * Creates a new RepositoryScanner. + * RepositoryFactory will be injected by the DI container. + */ + public RepositoryScanner(RepositoryFactory repositoryFactory) { + this.repositoryFactory = repositoryFactory; + } + + /** + * Scans for repository interfaces starting from the main class package. + * + * @return list of discovered repository interfaces + */ + public List> scanForRepositories() { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + String mainClassName = null; + + for (StackTraceElement element : stackTrace) { + if ("main".equals(element.getMethodName())) { + mainClassName = element.getClassName(); + break; + } + } + + if (mainClassName == null) { + logger.warning("Could not detect main class, scanning common packages for repositories"); + return scanCommonPackagesForRepositories(); + } + + String basePackage = getBasePackage(mainClassName); + logger.info("Scanning for repositories from package: " + basePackage); + + return scanPackageForRepositories(basePackage); + } + + /** + * Scans common package patterns when main class detection fails. + * + * @return list of repository interfaces found + */ + private List> scanCommonPackagesForRepositories() { + List> allRepositories = new ArrayList<>(); + String[] commonPackages = {"com", "org", "net", "examples"}; + + for (String pkg : commonPackages) { + try { + allRepositories.addAll(scanPackageForRepositories(pkg)); + } catch (Exception e) { + // Ignore packages that don't exist + } + } + + return allRepositories; + } + + /** + * Gets the base package from a fully qualified class name. + * + * @param className the fully qualified class name + * @return the base package name + */ + private String getBasePackage(String className) { + int lastDot = className.lastIndexOf('.'); + if (lastDot > 0) { + return className.substring(0, lastDot); + } + return ""; + } + + /** + * Scans the specified package for repository interfaces. + * + * @param packageName the base package to scan + * @return list of repository interfaces found + */ + public List> scanPackageForRepositories(String packageName) { + List> repositoryInterfaces = new ArrayList<>(); + + try { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + String path = packageName.replace('.', '/'); + Enumeration resources = classLoader.getResources(path); + + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + File file = new File(resource.getFile()); + + if (file.isDirectory()) { + repositoryInterfaces.addAll(findRepositoryInterfaces(file, packageName)); + } + } + } catch (IOException e) { + logger.severe("Error scanning package for repositories: " + packageName + " - " + e.getMessage()); + } + + if (!repositoryInterfaces.isEmpty()) { + logger.fine("Found " + repositoryInterfaces.size() + " repository interfaces in package: " + packageName); + } + return repositoryInterfaces; + } + + /** + * Recursively finds repository interfaces in the given directory. + * + * @param directory the directory to search + * @param packageName the package name corresponding to the directory + * @return list of repository interfaces found + */ + private List> findRepositoryInterfaces(File directory, String packageName) { + List> repositoryInterfaces = new ArrayList<>(); + + if (!directory.exists()) { + return repositoryInterfaces; + } + + File[] files = directory.listFiles(); + if (files == null) { + return repositoryInterfaces; + } + + for (File file : files) { + if (file.isDirectory()) { + repositoryInterfaces.addAll(findRepositoryInterfaces(file, packageName + "." + file.getName())); + } else if (file.getName().endsWith(".class")) { + String className = packageName + '.' + file.getName().substring(0, file.getName().length() - 6); + try { + Class clazz = Class.forName(className); + if (isRepositoryInterface(clazz)) { + repositoryInterfaces.add(clazz); + discoveredRepositories.add(clazz); + logger.info("Found repository interface: " + className); + } + } catch (ClassNotFoundException e) { + logger.warning("Could not load class: " + className); + } catch (NoClassDefFoundError e) { + logger.fine("Skipping class with missing dependencies: " + className); + } + } + } + + return repositoryInterfaces; + } + + /** + * Checks if a class is a repository interface (extends BaseRepository). + * + * @param clazz the class to check + * @return true if the class is a repository interface + */ + private boolean isRepositoryInterface(Class clazz) { + return clazz.isInterface() && + BaseRepository.class.isAssignableFrom(clazz) && + !BaseRepository.class.equals(clazz); + } + + /** + * Creates repository implementations for discovered interfaces. + * + * @return list of BeanDefinition objects for repository implementations + */ + public List createRepositoryBeans() { + List beanDefinitions = new ArrayList<>(); + + List> repositories = scanForRepositories(); + + for (Class repositoryInterface : repositories) { + try { + Object repositoryImpl = repositoryFactory.createRepository(repositoryInterface); + BeanDefinition beanDef = createRepositoryBeanDefinition(repositoryInterface, repositoryImpl); + beanDefinitions.add(beanDef); + + logger.fine("Created repository bean: " + repositoryInterface.getSimpleName()); + } catch (Exception e) { + logger.severe("Failed to create repository for interface: " + repositoryInterface.getName() + " - " + e.getMessage()); + } + } + + return beanDefinitions; + } + + /** + * Creates a BeanDefinition for a repository implementation. + * + * @param repositoryInterface the repository interface + * @param repositoryImpl the repository implementation + * @return the BeanDefinition + */ + private BeanDefinition createRepositoryBeanDefinition(Class repositoryInterface, Object repositoryImpl) { + RepositoryBeanWrapper wrapper = new RepositoryBeanWrapper(repositoryInterface, repositoryImpl); + + BeanDefinition beanDef = new BeanDefinition(wrapper.getClass()); + beanDef.setSingletonInstance(wrapper); + + return beanDef; + } + + /** + * Gets the list of discovered repository interfaces. + * + * @return list of discovered repositories + */ + public List> getDiscoveredRepositories() { + return new ArrayList<>(discoveredRepositories); + } + + /** + * Wrapper class for repository implementations to work with BeanDefinition. + */ + public static class RepositoryBeanWrapper { + private final Class repositoryInterface; + private final Object repositoryImpl; + + public RepositoryBeanWrapper(Class repositoryInterface, Object repositoryImpl) { + this.repositoryInterface = repositoryInterface; + this.repositoryImpl = repositoryImpl; + } + + public Class getRepositoryInterface() { + return repositoryInterface; + } + + public Object getRepositoryImpl() { + return repositoryImpl; + } + + @Override + public String toString() { + return "RepositoryBean[" + repositoryInterface.getSimpleName() + "]"; + } + } +} \ No newline at end of file diff --git a/src/main/java/jazzyframework/data/annotations/Modifying.java b/src/main/java/jazzyframework/data/annotations/Modifying.java new file mode 100644 index 0000000..d0bb01f --- /dev/null +++ b/src/main/java/jazzyframework/data/annotations/Modifying.java @@ -0,0 +1,52 @@ +package jazzyframework.data.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to indicate that a query method modifies the database. + * + *

This annotation should be used with {@code @Query} for UPDATE, DELETE, + * or INSERT operations. It tells the framework that the query will modify + * the database state and should be executed accordingly. + * + *

Examples: + *

+ * {@code @Query("UPDATE User u SET u.active = :active WHERE u.email = :email")}
+ * {@code @Modifying}
+ * int updateUserActiveStatus(String email, boolean active);
+ * 
+ * {@code @Query("DELETE FROM User u WHERE u.active = false")}
+ * {@code @Modifying}
+ * int deleteInactiveUsers();
+ * 
+ * + * @since 0.3.0 + * @author Caner Mastan + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Modifying { + + /** + * Whether to clear the persistence context after executing the modifying query. + * + *

If true, the persistence context will be cleared after the query execution, + * which ensures that subsequent queries will see the updated state. + * + * @return true to clear context, false otherwise + */ + boolean clearAutomatically() default false; + + /** + * Whether to flush the persistence context before executing the modifying query. + * + *

If true, pending changes will be flushed to the database before + * executing the modifying query. + * + * @return true to flush before execution, false otherwise + */ + boolean flushAutomatically() default false; +} \ No newline at end of file diff --git a/src/main/java/jazzyframework/data/annotations/Query.java b/src/main/java/jazzyframework/data/annotations/Query.java new file mode 100644 index 0000000..35f5971 --- /dev/null +++ b/src/main/java/jazzyframework/data/annotations/Query.java @@ -0,0 +1,80 @@ +package jazzyframework.data.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to define custom queries for repository methods. + * + *

This annotation allows you to define custom HQL/JPQL or native SQL queries + * for repository methods, similar to Spring Data JPA's @Query annotation. + * + *

Examples: + *

+ * // HQL/JPQL Query
+ * {@code @Query("SELECT u FROM User u WHERE u.email = :email AND u.active = true")}
+ * Optional<User> findActiveUserByEmail(String email);
+ * 
+ * // Native SQL Query
+ * {@code @Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true)}
+ * Optional<User> findByEmailNative(String email);
+ * 
+ * // Count Query
+ * {@code @Query("SELECT COUNT(u) FROM User u WHERE u.active = :active")}
+ * long countActiveUsers(boolean active);
+ * 
+ * // Update Query
+ * {@code @Query("UPDATE User u SET u.active = :active WHERE u.email = :email")}
+ * {@code @Modifying}
+ * int updateUserActiveStatus(String email, boolean active);
+ * 
+ * + * @since 0.3.0 + * @author Caner Mastan + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Query { + + /** + * The query string to execute. + * Can be HQL/JPQL (default) or native SQL (if nativeQuery = true). + * + *

Parameter binding: + *

    + *
  • Named parameters: {@code :paramName} (recommended)
  • + *
  • Positional parameters: {@code ?1, ?2, ...} (for native queries)
  • + *
+ * + * @return the query string + */ + String value(); + + /** + * Whether the query is a native SQL query. + * + *

If true, the query will be executed as native SQL. + * If false (default), the query will be executed as HQL/JPQL. + * + * @return true for native SQL, false for HQL/JPQL + */ + boolean nativeQuery() default false; + + /** + * The name of the named query to use. + * If specified, the value() will be ignored and the named query will be used instead. + * + * @return the named query name + */ + String name() default ""; + + /** + * Query hints to be applied to the query. + * These are JPA query hints that can be used to optimize query execution. + * + * @return array of query hints + */ + QueryHint[] hints() default {}; +} \ No newline at end of file diff --git a/src/main/java/jazzyframework/data/annotations/QueryHint.java b/src/main/java/jazzyframework/data/annotations/QueryHint.java new file mode 100644 index 0000000..1502827 --- /dev/null +++ b/src/main/java/jazzyframework/data/annotations/QueryHint.java @@ -0,0 +1,28 @@ +package jazzyframework.data.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Query hint annotation for providing hints to the query execution engine. + * + * @since 0.3.0 + * @author Caner Mastan + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface QueryHint { + + /** + * The name of the hint. + * + * @return the hint name + */ + String name(); + + /** + * The value of the hint. + * + * @return the hint value + */ + String value(); +} \ No newline at end of file diff --git a/src/main/java/jazzyframework/di/DIContainer.java b/src/main/java/jazzyframework/di/DIContainer.java index e227d3b..a828168 100644 --- a/src/main/java/jazzyframework/di/DIContainer.java +++ b/src/main/java/jazzyframework/di/DIContainer.java @@ -1,6 +1,10 @@ package jazzyframework.di; import jazzyframework.di.annotations.Named; +import jazzyframework.data.RepositoryScanner; +import jazzyframework.data.RepositoryFactory; +import jazzyframework.data.HibernateConfig; +import jazzyframework.core.PropertyLoader; import org.picocontainer.DefaultPicoContainer; import org.picocontainer.MutablePicoContainer; import org.picocontainer.PicoContainer; @@ -26,17 +30,21 @@ *

  • Lifecycle management with {@code @PostConstruct} and {@code @PreDestroy}
  • *
  • Singleton and Prototype scopes
  • *
  • Constructor-based dependency injection
  • + *
  • Database integration with automatic repository discovery
  • + *
  • Property-based configuration management
  • * * *

    All components are automatically discovered and registered at server startup time. * The container uses PicoContainer as the underlying DI engine while providing * Spring-like annotation support and automatic configuration. * - * @since 0.2 + * @since 0.3 * @author Caner Mastan */ public class DIContainer { private static final Logger logger = Logger.getLogger(DIContainer.class.getName()); + private static DIContainer instance; + private final MutablePicoContainer container; private final ComponentScanner scanner; final Map beanDefinitions; @@ -55,6 +63,18 @@ public DIContainer() { this.managedBeans = new ArrayList<>(); } + /** + * Gets the singleton instance of the DI container. + * + * @return the singleton DI container instance + */ + public static synchronized DIContainer getInstance() { + if (instance == null) { + instance = new DIContainer(); + } + return instance; + } + /** * Initializes the DI container with automatic component discovery. * Automatically scans all packages starting from the main class package. @@ -68,12 +88,22 @@ public void initialize() { logger.info("Initializing Advanced DI Container with automatic component discovery..."); + // First, register core infrastructure components + registerInfrastructureComponents(); + + // Scan for user components (@Component, @Service, @Controller, etc.) List componentBeans = scanner.scanAllPackages(); for (BeanDefinition beanDef : componentBeans) { registerBeanDefinition(beanDef); } + // Initialize database infrastructure if enabled + initializeDatabaseInfrastructure(); + + // Scan and register repositories + registerRepositories(); + validateDependencies(); initialized = true; @@ -81,6 +111,133 @@ public void initialize() { " components automatically discovered"); } + /** + * Registers core infrastructure components. + */ + private void registerInfrastructureComponents() { + logger.info("Registering core infrastructure components..."); + + // Register PropertyLoader as singleton + PropertyLoader propertyLoader = PropertyLoader.getInstance(); + BeanDefinition propertyLoaderDef = new BeanDefinition(PropertyLoader.class); + propertyLoaderDef.setSingletonInstance(propertyLoader); + registerBeanDefinition(propertyLoaderDef); + + logger.info("Core infrastructure components registered"); + } + + /** + * Initializes database infrastructure if enabled. + */ + private void initializeDatabaseInfrastructure() { + PropertyLoader propertyLoader = PropertyLoader.getInstance(); + + if (!propertyLoader.isDatabaseEnabled()) { + logger.info("Database is disabled, skipping database infrastructure initialization"); + return; + } + + logger.info("Initializing database infrastructure..."); + + try { + // Register HibernateConfig + BeanDefinition hibernateConfigDef = new BeanDefinition(HibernateConfig.class); + registerBeanDefinition(hibernateConfigDef); + + // Create and initialize HibernateConfig instance + HibernateConfig hibernateConfig = createBean(hibernateConfigDef); + hibernateConfigDef.setSingletonInstance(hibernateConfig); + + // Register RepositoryFactory + BeanDefinition repositoryFactoryDef = new BeanDefinition(RepositoryFactory.class); + registerBeanDefinition(repositoryFactoryDef); + + logger.info("Database infrastructure initialized successfully"); + } catch (Exception e) { + logger.severe("Failed to initialize database infrastructure: " + e.getMessage()); + throw new RuntimeException("Database initialization failed", e); + } + } + + /** + * Scans and registers repository interfaces. + */ + private void registerRepositories() { + PropertyLoader propertyLoader = PropertyLoader.getInstance(); + + if (!propertyLoader.isDatabaseEnabled()) { + logger.info("Database is disabled, skipping repository scanning"); + return; + } + + logger.info("Scanning for repository interfaces..."); + + try { + RepositoryFactory repositoryFactory = getComponentInternal(RepositoryFactory.class); + RepositoryScanner repositoryScanner = new RepositoryScanner(repositoryFactory); + + List repositoryBeans = repositoryScanner.createRepositoryBeans(); + + for (BeanDefinition repositoryBean : repositoryBeans) { + // Get the actual repository interface and implementation + RepositoryScanner.RepositoryBeanWrapper wrapper = + (RepositoryScanner.RepositoryBeanWrapper) repositoryBean.getSingletonInstance(); + + Class repositoryInterface = wrapper.getRepositoryInterface(); + Object repositoryImpl = wrapper.getRepositoryImpl(); + + // Create a proper bean definition for the repository interface + BeanDefinition repoDef = new BeanDefinition(repositoryInterface); + repoDef.setSingletonInstance(repositoryImpl); + + registerBeanDefinition(repoDef); + + logger.info("Registered repository: " + repositoryInterface.getSimpleName()); + } + + logger.info("Repository scanning completed. Found " + repositoryBeans.size() + " repositories"); + } catch (Exception e) { + logger.severe("Failed to scan repositories: " + e.getMessage()); + throw new RuntimeException("Repository scanning failed", e); + } + } + + /** + * Internal method to get component without initialization check. + * Used during container initialization to avoid chicken-and-egg problems. + * + * @param type the class type + * @param the type parameter + * @return the component instance with dependencies injected + */ + private T getComponentInternal(Class type) { + List candidates = typeIndex.get(type); + if (candidates == null || candidates.isEmpty()) { + return createInstance(type); + } + + BeanDefinition beanDef = selectCandidate(candidates, type); + return createBean(beanDef); + } + + /** + * Internal method to get component by name without initialization check. + * Used during container initialization to avoid chicken-and-egg problems. + * + * @param name the component name + * @param the type parameter + * @return the component instance + */ + @SuppressWarnings("unchecked") + private T getComponentInternal(String name) { + BeanDefinition beanDef = beanDefinitions.get(name); + if (beanDef == null) { + throw new IllegalArgumentException("No bean found with name: " + name); + } + + return (T) createBean(beanDef); + } + /** * Registers a bean definition and builds indexes. * @@ -255,9 +412,9 @@ private T createInstanceWithDI(Class type) { Named namedAnnotation = param.getAnnotation(Named.class); if (namedAnnotation != null) { - args[i] = getComponent(namedAnnotation.value()); + args[i] = getComponentInternal(namedAnnotation.value()); } else { - args[i] = getComponent(paramType); + args[i] = getComponentInternal(paramType); } } @@ -347,6 +504,14 @@ public void dispose() { logger.info("DI Container disposed"); } + /** + * Shuts down the container and releases all resources. + * This is an alias for dispose() method. + */ + public void shutdown() { + dispose(); + } + /** * Calls @PreDestroy methods on the bean. */ diff --git a/src/main/java/jazzyframework/di/annotations/PostConstruct.java b/src/main/java/jazzyframework/di/annotations/PostConstruct.java index 6441fda..038c20d 100644 --- a/src/main/java/jazzyframework/di/annotations/PostConstruct.java +++ b/src/main/java/jazzyframework/di/annotations/PostConstruct.java @@ -6,30 +6,47 @@ import java.lang.annotation.Target; /** - * Indicates that a method should be called after dependency injection is complete. - * The annotated method will be invoked after the object has been constructed and - * all dependencies have been injected. + * Annotation to mark methods that should be called after dependency injection. * - *

    The method must be public, have no parameters, and return void. + *

    Methods annotated with {@code @PostConstruct} are called automatically + * by the DI container after all dependencies have been injected and the + * object is fully constructed. + * + *

    This is useful for: + *

      + *
    • Initializing resources that depend on injected dependencies
    • + *
    • Setting up configuration based on injected components
    • + *
    • Starting background processes or connections
    • + *
    • Validating the component's state after construction
    • + *
    + * + *

    Requirements: + *

      + *
    • The method must have no parameters
    • + *
    • The method must not be static
    • + *
    • The method may have any access modifier
    • + *
    • The method must not throw checked exceptions
    • + *
    * *

    Example usage: *

      * @Component
      * public class UserService {
    + *     @Inject
      *     private UserRepository userRepository;
      *     
    - *     public UserService(UserRepository userRepository) {
    - *         this.userRepository = userRepository;
    - *     }
    - *     
      *     @PostConstruct
      *     public void initialize() {
    - *         // Initialization code here
    - *         System.out.println("UserService initialized!");
    - *         // Load default data, connect to external services, etc.
    + *         // Perform initialization that requires injected dependencies
    + *         if (userRepository.count() == 0) {
    + *             createDefaultUsers();
    + *         }
      *     }
      * }
      * 
    + * + * @since 0.2 + * @author Caner Mastan */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) diff --git a/src/main/java/jazzyframework/di/annotations/PreDestroy.java b/src/main/java/jazzyframework/di/annotations/PreDestroy.java index fd6212b..3d8abc3 100644 --- a/src/main/java/jazzyframework/di/annotations/PreDestroy.java +++ b/src/main/java/jazzyframework/di/annotations/PreDestroy.java @@ -6,11 +6,28 @@ import java.lang.annotation.Target; /** - * Indicates that a method should be called before the component is removed from the container. - * The annotated method will be invoked when the container is being disposed or the - * component is being removed. + * Annotation to mark methods that should be called before component destruction. * - *

    The method must be public, have no parameters, and return void. + *

    Methods annotated with {@code @PreDestroy} are called automatically + * by the DI container when the application is shutting down or the component + * is being removed from the container. + * + *

    This is useful for: + *

      + *
    • Closing database connections and resources
    • + *
    • Stopping background threads or scheduled tasks
    • + *
    • Releasing file handles or network connections
    • + *
    • Persisting state or flushing caches
    • + *
    • Cleaning up temporary files or directories
    • + *
    + * + *

    Requirements: + *

      + *
    • The method must have no parameters
    • + *
    • The method must not be static
    • + *
    • The method may have any access modifier
    • + *
    • The method should not throw checked exceptions
    • + *
    * *

    Example usage: *

    @@ -20,19 +37,24 @@
      *     
      *     @PostConstruct
      *     public void connect() {
    - *         connection = DriverManager.getConnection("...");
    + *         this.connection = DriverManager.getConnection(...);
      *     }
      *     
      *     @PreDestroy
      *     public void disconnect() {
    - *         // Cleanup code here
      *         if (connection != null) {
    - *             connection.close();
    + *             try {
    + *                 connection.close();
    + *             } catch (SQLException e) {
    + *                 logger.warning("Error closing connection: " + e.getMessage());
    + *             }
      *         }
    - *         System.out.println("DatabaseService cleaned up!");
      *     }
      * }
      * 
    + * + * @since 0.2 + * @author Caner Mastan */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) diff --git a/src/main/java/jazzyframework/http/Request.java b/src/main/java/jazzyframework/http/Request.java index 7fb3710..8084254 100644 --- a/src/main/java/jazzyframework/http/Request.java +++ b/src/main/java/jazzyframework/http/Request.java @@ -1,288 +1,271 @@ -/** - * Represents an HTTP request in the Jazzy framework. - * Provides convenient access to request parameters, headers, path, query, and body. - */ -package jazzyframework.http; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import jazzyframework.http.validation.ValidationResult; -import jazzyframework.http.validation.ValidationRules; -import jazzyframework.http.validation.Validator; - -public class Request { - private final String method; - private final String path; - private final Map headers; - private final Map pathParams; - private final Map queryParams; - private final String body; - - /** - * Creates a new Request object. - * - * @param method The HTTP method (GET, POST, etc.) - * @param path The request path - * @param headers The request headers - * @param pathParams The path parameters - * @param queryParams The query parameters - * @param body The request body - */ - public Request(String method, String path, Map headers, - Map pathParams, Map queryParams, String body) { - this.method = method; - this.path = path; - this.headers = headers != null ? new HashMap<>(headers) : new HashMap<>(); - this.pathParams = pathParams != null ? new HashMap<>(pathParams) : new HashMap<>(); - this.queryParams = queryParams != null ? new HashMap<>(queryParams) : new HashMap<>(); - this.body = body; - } - - /** - * Gets the HTTP method. - * - * @return The HTTP method - */ - public String getMethod() { - return method; - } - - /** - * Gets the request path. - * - * @return The request path - */ - public String getPath() { - return path; - } - - /** - * Gets a request header. - * - * @param name The header name - * @return The header value, or null if not found - */ - public String header(String name) { - return headers.get(name.toLowerCase()); - } - - /** - * Gets a request header with a default value. - * - * @param name The header name - * @param defaultValue The default value to return if the header is not found - * @return The header value, or the default value if not found - */ - public String header(String name, String defaultValue) { - return headers.getOrDefault(name.toLowerCase(), defaultValue); - } - - /** - * Gets all request headers. - * - * @return An unmodifiable map of all headers - */ - public Map getHeaders() { - return Collections.unmodifiableMap(headers); - } - - /** - * Gets a path parameter. - * - * @param name The parameter name - * @return The parameter value, or null if not found - */ - public String path(String name) { - return pathParams.get(name); - } - - /** - * Gets all path parameters. - * - * @return An unmodifiable map of all path parameters - */ - public Map getPathParams() { - return Collections.unmodifiableMap(pathParams); - } - - /** - * Gets a query parameter. - * - * @param name The parameter name - * @return The parameter value, or null if not found - */ - public String query(String name) { - return queryParams.get(name); - } - - /** - * Gets a query parameter with a default value. - * - * @param name The parameter name - * @param defaultValue The default value to return if the parameter is not found - * @return The parameter value, or the default value if not found - */ - public String query(String name, String defaultValue) { - return queryParams.getOrDefault(name, defaultValue); - } - - /** - * Gets a query parameter as an integer. - * - * @param name The parameter name - * @param defaultValue The default value to return if the parameter is not found or invalid - * @return The parameter value as an integer, or the default value - */ - public int queryInt(String name, int defaultValue) { - String value = queryParams.get(name); - if (value == null) { - return defaultValue; - } - try { - return Integer.parseInt(value); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - /** - * Gets a query parameter as a boolean. - * - * @param name The parameter name - * @param defaultValue The default value to return if the parameter is not found - * @return The parameter value as a boolean, or the default value - */ - public boolean queryBoolean(String name, boolean defaultValue) { - String value = queryParams.get(name); - if (value == null) { - return defaultValue; - } - return Boolean.parseBoolean(value); - } - - /** - * Gets all query parameters. - * - * @return An unmodifiable map of all query parameters - */ - public Map getQueryParams() { - return Collections.unmodifiableMap(queryParams); - } - - /** - * Gets the request body as a string. - * - * @return The request body - */ - public String getBody() { - return body; - } - - /** - * Parses the request body as JSON and returns it as a Map. - * - * @return The parsed JSON as a Map, or an empty Map if the body is empty or not valid JSON - */ - public Map parseJson() { - if (body == null || body.isEmpty()) { - return Collections.emptyMap(); - } - - try { - return JsonParser.parseMap(body); - } catch (Exception e) { - throw new IllegalArgumentException("Invalid JSON body: " + e.getMessage()); - } - } - - /** - * Parses the request body as JSON and returns it as an object of the specified class. - * - * @param The type to convert to - * @param clazz The class of the target object - * @return The parsed JSON as an object - */ - public T toObject(Class clazz) { - if (body == null || body.isEmpty()) { - throw new IllegalArgumentException("Request body is empty"); - } - - try { - return JsonParser.parse(body, clazz); - } catch (Exception e) { - throw new IllegalArgumentException("Invalid JSON body: " + e.getMessage()); - } - } - - /** - * @deprecated Use {@link #parseJson()} instead - */ - @Deprecated - public Map json() { - return parseJson(); - } - - /** - * @deprecated Use {@link #toObject(Class)} instead - */ - @Deprecated - public T json(Class clazz) { - return toObject(clazz); - } - - /** - * Creates a validator for this request. - * - * @return A new validator - */ - public Validator validator() { - String contentType = header("Content-Type"); - boolean isJson = contentType != null && contentType.toLowerCase().contains("application/json"); - - Map dataToValidate = new HashMap<>(); - - // Add query parameters to data - for (Map.Entry entry : queryParams.entrySet()) { - dataToValidate.put(entry.getKey(), entry.getValue()); - } - - // If there's JSON body, add those fields too, potentially overriding query params - if (isJson && body != null && !body.isEmpty()) { - try { - Map jsonData = parseJson(); - dataToValidate.putAll(jsonData); - } catch (Exception e) { - // Ignore JSON parsing errors in validation - } - } - - return new Validator(dataToValidate); - } - - /** - * Validates this request using the specified validation rules. - * - * @param rules The validation rules to apply - * @return The validation result - */ - public ValidationResult validate(ValidationRules rules) { - Validator validator = validator(); - rules.setValidator(validator); - return rules.validate(); - } - - /** - * Validates this request and throws an exception if validation fails. - * - * @param rules The validation rules to apply - * @throws IllegalArgumentException if validation fails - */ - public void validateOrFail(ValidationRules rules) { - ValidationResult result = validate(rules); - if (!result.isValid()) { - throw new IllegalArgumentException("Validation failed: " + result.getFirstErrors()); - } - } +/** + * Represents an HTTP request in the Jazzy framework. + * Provides convenient access to request parameters, headers, path, query, and body. + */ +package jazzyframework.http; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import jazzyframework.http.validation.ValidationResult; +import jazzyframework.http.validation.ValidationRules; +import jazzyframework.http.validation.Validator; + +public class Request { + private final String method; + private final String path; + private final Map headers; + private final Map pathParams; + private final Map queryParams; + private final String body; + + /** + * Creates a new Request object. + * + * @param method The HTTP method (GET, POST, etc.) + * @param path The request path + * @param headers The request headers + * @param pathParams The path parameters + * @param queryParams The query parameters + * @param body The request body + */ + public Request(String method, String path, Map headers, + Map pathParams, Map queryParams, String body) { + this.method = method; + this.path = path; + this.headers = headers != null ? new HashMap<>(headers) : new HashMap<>(); + this.pathParams = pathParams != null ? new HashMap<>(pathParams) : new HashMap<>(); + this.queryParams = queryParams != null ? new HashMap<>(queryParams) : new HashMap<>(); + this.body = body; + } + + /** + * Gets the HTTP method. + * + * @return The HTTP method + */ + public String getMethod() { + return method; + } + + /** + * Gets the request path. + * + * @return The request path + */ + public String getPath() { + return path; + } + + /** + * Gets a request header. + * + * @param name The header name + * @return The header value, or null if not found + */ + public String header(String name) { + return headers.get(name.toLowerCase()); + } + + /** + * Gets a request header with a default value. + * + * @param name The header name + * @param defaultValue The default value to return if the header is not found + * @return The header value, or the default value if not found + */ + public String header(String name, String defaultValue) { + return headers.getOrDefault(name.toLowerCase(), defaultValue); + } + + /** + * Gets all request headers. + * + * @return An unmodifiable map of all headers + */ + public Map getHeaders() { + return Collections.unmodifiableMap(headers); + } + + /** + * Gets a path parameter. + * + * @param name The parameter name + * @return The parameter value, or null if not found + */ + public String path(String name) { + return pathParams.get(name); + } + + /** + * Gets all path parameters. + * + * @return An unmodifiable map of all path parameters + */ + public Map getPathParams() { + return Collections.unmodifiableMap(pathParams); + } + + /** + * Gets a query parameter. + * + * @param name The parameter name + * @return The parameter value, or null if not found + */ + public String query(String name) { + return queryParams.get(name); + } + + /** + * Gets a query parameter with a default value. + * + * @param name The parameter name + * @param defaultValue The default value to return if the parameter is not found + * @return The parameter value, or the default value if not found + */ + public String query(String name, String defaultValue) { + return queryParams.getOrDefault(name, defaultValue); + } + + /** + * Gets a query parameter as an integer. + * + * @param name The parameter name + * @param defaultValue The default value to return if the parameter is not found or invalid + * @return The parameter value as an integer, or the default value + */ + public int queryInt(String name, int defaultValue) { + String value = queryParams.get(name); + if (value == null) { + return defaultValue; + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + /** + * Gets a query parameter as a boolean. + * + * @param name The parameter name + * @param defaultValue The default value to return if the parameter is not found + * @return The parameter value as a boolean, or the default value + */ + public boolean queryBoolean(String name, boolean defaultValue) { + String value = queryParams.get(name); + if (value == null) { + return defaultValue; + } + return Boolean.parseBoolean(value); + } + + /** + * Gets all query parameters. + * + * @return An unmodifiable map of all query parameters + */ + public Map getQueryParams() { + return Collections.unmodifiableMap(queryParams); + } + + /** + * Gets the request body as a string. + * + * @return The request body + */ + public String getBody() { + return body; + } + + /** + * Parses the request body as JSON and returns it as a Map. + * + * @return The parsed JSON as a Map, or an empty Map if the body is empty or not valid JSON + */ + public Map parseJson() { + if (body == null || body.isEmpty()) { + return Collections.emptyMap(); + } + + try { + return JsonParser.parseMap(body); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid JSON body: " + e.getMessage()); + } + } + + /** + * Parses the request body as JSON and returns it as a Map. + * This is an alias for parseJson() method. + * + * @return The parsed JSON as a Map, or an empty Map if the body is empty or not valid JSON + */ + public Map json() { + return parseJson(); + } + + /** + * Parses the request body as JSON and returns it as an object of the specified class. + * + * @param The type to convert to + * @param clazz The class of the target object + * @return The parsed JSON as an object + */ + public T toObject(Class clazz) { + if (body == null || body.isEmpty()) { + throw new IllegalArgumentException("Request body is empty"); + } + + try { + return JsonParser.parse(body, clazz); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid JSON body: " + e.getMessage()); + } + } + + /** + * Creates a validator for this request. + * + * @return A new validator + */ + public Validator validator() { + return new Validator(this); + } + + /** + * Creates a validator for this request. + * + * @return A new validator + */ + public Validator validate() { + return new Validator(this); + } + + /** + * Validates this request using the specified validation rules. + * + * @param rules The validation rules to apply + * @return The validation result + */ + public ValidationResult validate(ValidationRules rules) { + Validator validator = validator(); + rules.setValidator(validator); + return rules.validate(); + } + + /** + * Validates this request and throws an exception if validation fails. + * + * @param rules The validation rules to apply + * @throws IllegalArgumentException if validation fails + */ + public void validateOrFail(ValidationRules rules) { + ValidationResult result = validate(rules); + if (!result.isValid()) { + throw new IllegalArgumentException("Validation failed: " + result.getFirstErrors()); + } + } } \ No newline at end of file diff --git a/src/main/java/jazzyframework/http/validation/Validator.java b/src/main/java/jazzyframework/http/validation/Validator.java index e14bd57..9b612d6 100644 --- a/src/main/java/jazzyframework/http/validation/Validator.java +++ b/src/main/java/jazzyframework/http/validation/Validator.java @@ -10,6 +10,8 @@ import java.util.List; import java.util.Map; +import jazzyframework.http.Request; + public class Validator { private final Map data; private final Map> rules; @@ -31,6 +33,37 @@ public Validator(Map data) { } } + /** + * Creates a new Validator for the specified Request. + * Extracts validation data from query parameters, path parameters, and JSON body. + * + * @param request The request to validate + */ + public Validator(Request request) { + this.data = new HashMap<>(); + this.rules = new HashMap<>(); + + // Add query parameters + if (request.getQueryParams() != null) { + this.data.putAll(request.getQueryParams()); + } + + // Add path parameters + if (request.getPathParams() != null) { + this.data.putAll(request.getPathParams()); + } + + // Add JSON body data if present + try { + Map jsonData = request.parseJson(); + if (jsonData != null) { + this.data.putAll(jsonData); + } + } catch (Exception e) { + // Ignore JSON parsing errors during validation setup + } + } + /** * Begins validation for a field. * diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..d365ffc --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,124 @@ +# JazzyFramework Database Configuration +# This file demonstrates how to configure database connections for your application. +# The framework supports H2 (embedded), MySQL, and PostgreSQL out of the box. + +# ============================================================================ +# DATABASE CONFIGURATION +# ============================================================================ + +# Enable/disable database functionality +jazzy.database.enabled=true + +# ============================================================================ +# H2 DATABASE (Default - for development) +# ============================================================================ +# H2 is automatically configured if no other database is specified +# Uncomment and modify these settings to customize H2 behavior + +jazzy.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +jazzy.datasource.driver-class-name=org.h2.Driver +jazzy.datasource.username=sa +jazzy.datasource.password= + +# H2 Console (accessible at http://localhost:8082) +jazzy.h2.console.enabled=true + +# ============================================================================ +# MYSQL DATABASE (Production example) +# ============================================================================ +# Uncomment these lines to use MySQL instead of H2 +# Make sure you have MySQL running and the database created + +#jazzy.datasource.url=jdbc:mysql://localhost:3306/jazzy_app?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC +#jazzy.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +#jazzy.datasource.username=your_username +#jazzy.datasource.password=your_password + +# ============================================================================ +# POSTGRESQL DATABASE (Production example) +# ============================================================================ +# Uncomment these lines to use PostgreSQL instead of H2 +# Make sure you have PostgreSQL running and the database created + +#jazzy.datasource.url=jdbc:postgresql://localhost:5432/jazzy_app +#jazzy.datasource.driver-class-name=org.postgresql.Driver +#jazzy.datasource.username=your_username +#jazzy.datasource.password=your_password + +# ============================================================================ +# HIBERNATE/JPA CONFIGURATION +# ============================================================================ + +# Database platform (automatically detected if not specified) +jazzy.jpa.database-platform=org.hibernate.dialect.H2Dialect + +# DDL mode: none, validate, update, create, create-drop +jazzy.jpa.hibernate.ddl-auto=update + +# Show SQL queries in console (useful for debugging) +jazzy.jpa.show-sql=true + +# Format SQL queries for better readability +jazzy.jpa.properties.hibernate.format_sql=true + +# Additional Hibernate properties +jazzy.jpa.properties.hibernate.use_sql_comments=true +jazzy.jpa.properties.hibernate.jdbc.batch_size=20 +jazzy.jpa.properties.hibernate.order_inserts=true +jazzy.jpa.properties.hibernate.order_updates=true + +# ============================================================================ +# CONNECTION POOL CONFIGURATION (HikariCP) +# ============================================================================ + +# Maximum number of connections in the pool +jazzy.datasource.hikari.maximum-pool-size=10 + +# Minimum number of idle connections +jazzy.datasource.hikari.minimum-idle=2 + +# Connection timeout (milliseconds) +jazzy.datasource.hikari.connection-timeout=30000 + +# Idle timeout (milliseconds) +jazzy.datasource.hikari.idle-timeout=600000 + +# Maximum lifetime of a connection (milliseconds) +jazzy.datasource.hikari.max-lifetime=1800000 + +# ============================================================================ +# EXAMPLE CONFIGURATIONS FOR DIFFERENT ENVIRONMENTS +# ============================================================================ + +# Development (default - uses H2) +# - No additional configuration needed +# - H2 console available at http://localhost:8082 +# - Database file is in-memory (data lost on restart) + +# Testing (persistent H2 file) +# jazzy.datasource.url=jdbc:h2:file:./data/testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + +# Staging (MySQL example) +# jazzy.datasource.url=jdbc:mysql://staging-db:3306/jazzy_staging +# jazzy.datasource.username=staging_user +# jazzy.datasource.password=staging_password +# jazzy.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect + +# Production (PostgreSQL example) +# jazzy.datasource.url=jdbc:postgresql://prod-db:5432/jazzy_production +# jazzy.datasource.username=prod_user +# jazzy.datasource.password=secure_password +# jazzy.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +# jazzy.jpa.hibernate.ddl-auto=validate +# jazzy.jpa.show-sql=false + +# ============================================================================ +# NOTES +# ============================================================================ +# +# 1. Change jazzy.jpa.hibernate.ddl-auto to "validate" in production +# 2. Set jazzy.jpa.show-sql to false in production for performance +# 3. Always use strong passwords in production environments +# 4. Consider using environment variables for sensitive data +# 5. The framework automatically detects and registers @Entity classes +# 6. Repository interfaces extending BaseRepository are auto-implemented \ No newline at end of file diff --git a/src/test/java/jazzyframework/core/PropertyLoaderTest.java b/src/test/java/jazzyframework/core/PropertyLoaderTest.java new file mode 100644 index 0000000..58e7b98 --- /dev/null +++ b/src/test/java/jazzyframework/core/PropertyLoaderTest.java @@ -0,0 +1,186 @@ +package jazzyframework.core; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.AfterEach; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * Tests for PropertyLoader class focusing on database configuration. + */ +public class PropertyLoaderTest { + + @BeforeEach + public void setUp() { + // Reset singleton instance before each test + resetPropertyLoaderInstance(); + } + + @AfterEach + public void tearDown() { + // Clean up after each test + resetPropertyLoaderInstance(); + } + + @Test + public void testGetDatabaseUrl_DefaultValue() { + PropertyLoader loader = PropertyLoader.getInstance(); + String url = loader.getDatabaseUrl(); + assertEquals("jdbc:h2:mem:test_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE", url); + } + + @Test + public void testGetDatabaseUsername_DefaultValue() { + PropertyLoader loader = PropertyLoader.getInstance(); + String username = loader.getDatabaseUsername(); + assertEquals("sa", username); + } + + @Test + public void testGetDatabasePassword_DefaultValue() { + PropertyLoader loader = PropertyLoader.getInstance(); + String password = loader.getDatabasePassword(); + assertEquals("", password); + } + + @Test + public void testGetDatabaseDriverClassName_DefaultValue() { + PropertyLoader loader = PropertyLoader.getInstance(); + String driver = loader.getDatabaseDriverClassName(); + assertEquals("org.h2.Driver", driver); + } + + @Test + public void testGetHibernateDialect_DefaultValue() { + PropertyLoader loader = PropertyLoader.getInstance(); + String dialect = loader.getHibernateDialect(); + assertEquals("org.hibernate.dialect.H2Dialect", dialect); + } + + @Test + public void testGetHibernateDdlAuto_DefaultValue() { + PropertyLoader loader = PropertyLoader.getInstance(); + String ddlAuto = loader.getHibernateDdlAuto(); + assertEquals("create-drop", ddlAuto); + } + + @Test + public void testIsShowSql_DefaultValue() { + PropertyLoader loader = PropertyLoader.getInstance(); + boolean showSql = loader.isShowSql(); + assertFalse(showSql); + } + + @Test + public void testIsFormatSql_DefaultValue() { + PropertyLoader loader = PropertyLoader.getInstance(); + boolean formatSql = loader.isFormatSql(); + assertFalse(formatSql); + } + + @Test + public void testIsH2ConsoleEnabled_DefaultValue() { + PropertyLoader loader = PropertyLoader.getInstance(); + boolean h2Console = loader.isH2ConsoleEnabled(); + assertFalse(h2Console); + } + + @Test + public void testIsDatabaseEnabled_DefaultValue() { + PropertyLoader loader = PropertyLoader.getInstance(); + boolean dbEnabled = loader.isDatabaseEnabled(); + assertTrue(dbEnabled); + } + + @Test + public void testBooleanProperty_ValidValues() { + PropertyLoader loader = PropertyLoader.getInstance(); + + // Test valid boolean values from test.properties + assertTrue(loader.getBooleanProperty("test.boolean.true", false)); + assertFalse(loader.getBooleanProperty("test.boolean.false", true)); + + // Test default values + assertTrue(loader.getBooleanProperty("non.existent.property", true)); + assertFalse(loader.getBooleanProperty("non.existent.property", false)); + } + + @Test + public void testIntProperty_ValidValues() { + PropertyLoader loader = PropertyLoader.getInstance(); + + // Test default values for non-existent properties + assertEquals(100, loader.getIntProperty("non.existent.property", 100)); + assertEquals(42, loader.getIntProperty("non.existent.property", 42)); + } + + @Test + public void testStringProperty_DefaultHandling() { + PropertyLoader loader = PropertyLoader.getInstance(); + + // Test default values + assertEquals("default", loader.getProperty("non.existent.property", "default")); + assertNull(loader.getProperty("non.existent.property")); + } + + @Test + public void testHasProperty() { + PropertyLoader loader = PropertyLoader.getInstance(); + + // These properties should exist with default values + assertTrue(loader.hasProperty("jazzy.datasource.url") || + !loader.hasProperty("jazzy.datasource.url")); // Either way is valid + + // This property should definitely not exist + assertFalse(loader.hasProperty("definitely.non.existent.property.12345")); + } + + @Test + public void testGetAllProperties() { + PropertyLoader loader = PropertyLoader.getInstance(); + Properties props = loader.getAllProperties(); + + assertNotNull(props); + // Properties should be independent copy + assertNotSame(props, loader.getAllProperties()); + } + + @Test + public void testPropertyPrefixes() { + PropertyLoader loader = PropertyLoader.getInstance(); + + // Test that all database methods use "jazzy" prefix correctly + // We can't test the actual property values without a test properties file, + // but we can test that the methods work without exceptions + assertDoesNotThrow(() -> { + loader.getDatabaseUrl(); + loader.getDatabaseUsername(); + loader.getDatabasePassword(); + loader.getDatabaseDriverClassName(); + loader.getHibernateDialect(); + loader.getHibernateDdlAuto(); + loader.isShowSql(); + loader.isFormatSql(); + loader.isH2ConsoleEnabled(); + loader.isDatabaseEnabled(); + }); + } + + /** + * Helper method to reset PropertyLoader singleton for testing. + * Uses reflection to reset the static instance. + */ + private void resetPropertyLoaderInstance() { + try { + var field = PropertyLoader.class.getDeclaredField("instance"); + field.setAccessible(true); + field.set(null, null); + } catch (Exception e) { + // Ignore reflection errors in test + } + } +} \ No newline at end of file diff --git a/src/test/java/jazzyframework/data/EntityScannerTest.java b/src/test/java/jazzyframework/data/EntityScannerTest.java new file mode 100644 index 0000000..acbf916 --- /dev/null +++ b/src/test/java/jazzyframework/data/EntityScannerTest.java @@ -0,0 +1,135 @@ +package jazzyframework.data; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +/** + * Tests for EntityScanner class. + */ +public class EntityScannerTest { + + private EntityScanner entityScanner; + + @BeforeEach + public void setUp() { + entityScanner = new EntityScanner(); + } + + @Test + public void testScanForEntities() { + List> entities = entityScanner.scanForEntities(); + + assertNotNull(entities); + // Should find at least the User entity from examples + assertTrue(entities.size() >= 0); // May be 0 if no entities in test classpath + } + + @Test + public void testScanSpecificPackageForEntities() { + // Test scanning a specific package + List> entities = entityScanner.scanPackageForEntities("examples.database.entity"); + + assertNotNull(entities); + // Results depend on whether examples are in test classpath + } + + @Test + public void testScanMultiplePackagesForEntities() { + String[] packages = {"examples.database.entity", "com.example", "org.test"}; + List> entities = entityScanner.scanPackagesForEntities(packages); + + assertNotNull(entities); + } + + @Test + public void testValidateEntity_ValidEntity() { + boolean isValid = entityScanner.validateEntity(TestEntity.class); + assertTrue(isValid); + } + + @Test + public void testValidateEntity_InvalidEntity() { + boolean isValid = entityScanner.validateEntity(NonEntity.class); + assertFalse(isValid); + } + + @Test + public void testValidateEntity_EntityWithoutDefaultConstructor() { + boolean isValid = entityScanner.validateEntity(EntityWithoutDefaultConstructor.class); + assertFalse(isValid); + } + + @Test + public void testScanPackageForEntities_NonExistentPackage() { + List> entities = entityScanner.scanPackageForEntities("com.nonexistent.package"); + + assertNotNull(entities); + assertTrue(entities.isEmpty()); + } + + @Test + public void testScanPackagesForEntities_EmptyArray() { + List> entities = entityScanner.scanPackagesForEntities(); + + assertNotNull(entities); + assertTrue(entities.isEmpty()); + } + + // Test entities for validation + + @Entity + public static class TestEntity { + @Id + private Long id; + private String name; + + public TestEntity() { + // Default constructor + } + + public TestEntity(String name) { + this.name = name; + } + + // Getters and setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + + // Non-entity class for testing + public static class NonEntity { + private Long id; + private String name; + + public NonEntity() {} + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + + // Entity without default constructor + @Entity + public static class EntityWithoutDefaultConstructor { + @Id + private Long id; + private String name; + + public EntityWithoutDefaultConstructor(String name) { + this.name = name; + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } +} \ No newline at end of file diff --git a/src/test/java/jazzyframework/data/HibernateConfigTest.java b/src/test/java/jazzyframework/data/HibernateConfigTest.java new file mode 100644 index 0000000..34b9e9e --- /dev/null +++ b/src/test/java/jazzyframework/data/HibernateConfigTest.java @@ -0,0 +1,81 @@ +package jazzyframework.data; + +import jazzyframework.core.PropertyLoader; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests for HibernateConfig class. + */ +@ExtendWith(MockitoExtension.class) +public class HibernateConfigTest { + + private HibernateConfig hibernateConfig; + + @BeforeEach + public void setUp() { + hibernateConfig = new HibernateConfig(); + } + + @Test + public void testConstructor() { + assertNotNull(hibernateConfig); + assertFalse(hibernateConfig.isInitialized()); + } + + @Test + public void testIsInitialized_BeforeInit() { + assertFalse(hibernateConfig.isInitialized()); + } + + @Test + public void testGetSessionFactory_NotInitialized() { + assertThrows(IllegalStateException.class, () -> { + hibernateConfig.getSessionFactory(); + }); + } + + @Test + public void testGetDataSource_NotInitialized() { + assertThrows(IllegalStateException.class, () -> { + hibernateConfig.getDataSource(); + }); + } + + @Test + public void testInitialize_DatabaseDisabled() { + // Mock PropertyLoader to return database disabled + try (MockedStatic mockedPropertyLoader = mockStatic(PropertyLoader.class)) { + PropertyLoader mockLoader = mock(PropertyLoader.class); + when(mockLoader.isDatabaseEnabled()).thenReturn(false); + mockedPropertyLoader.when(PropertyLoader::getInstance).thenReturn(mockLoader); + + // Create new instance to use mocked PropertyLoader + HibernateConfig config = new HibernateConfig(); + + // This should not throw and should not initialize + assertDoesNotThrow(() -> config.initialize()); + assertFalse(config.isInitialized()); + } + } + + @Test + public void testDestroy() { + // Test that destroy doesn't throw even when not initialized + assertDoesNotThrow(() -> hibernateConfig.destroy()); + } + + @Test + public void testDestroy_AfterPartialInit() { + // Even after partial initialization, destroy should work + assertDoesNotThrow(() -> hibernateConfig.destroy()); + } + + // Note: Full initialization tests are covered in integration tests + // because they require actual database setup which is complex for unit tests +} \ No newline at end of file diff --git a/src/test/java/jazzyframework/data/RepositoryFactoryTest.java b/src/test/java/jazzyframework/data/RepositoryFactoryTest.java new file mode 100644 index 0000000..9716434 --- /dev/null +++ b/src/test/java/jazzyframework/data/RepositoryFactoryTest.java @@ -0,0 +1,165 @@ +package jazzyframework.data; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.SessionFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests for RepositoryFactory class. + */ +@ExtendWith(MockitoExtension.class) +public class RepositoryFactoryTest { + + @Mock + private HibernateConfig hibernateConfig; + + @Mock + private SessionFactory sessionFactory; + + private RepositoryFactory repositoryFactory; + + @BeforeEach + public void setUp() { + repositoryFactory = new RepositoryFactory(hibernateConfig); + + // Setup mocks + when(hibernateConfig.isInitialized()).thenReturn(true); + when(hibernateConfig.getSessionFactory()).thenReturn(sessionFactory); + + // Initialize the factory + repositoryFactory.initialize(); + } + + @Test + public void testCreateRepository_ValidInterface() { + TestRepository repository = repositoryFactory.createRepository(TestRepository.class); + + assertNotNull(repository); + // Verify it's a proxy + assertTrue(repository.getClass().getName().contains("Proxy")); + } + + @Test + public void testCreateRepository_InvalidInterface() { + assertThrows(IllegalArgumentException.class, () -> { + repositoryFactory.createRepository(InvalidRepository.class); + }); + } + + @Test + public void testCreateRepository_Caching() { + TestRepository repo1 = repositoryFactory.createRepository(TestRepository.class); + TestRepository repo2 = repositoryFactory.createRepository(TestRepository.class); + + assertSame(repo1, repo2); // Should return same cached instance + } + + @Test + public void testHasRepository() { + assertFalse(repositoryFactory.hasRepository(TestRepository.class)); + + repositoryFactory.createRepository(TestRepository.class); + + assertTrue(repositoryFactory.hasRepository(TestRepository.class)); + } + + @Test + public void testClearCache() { + repositoryFactory.createRepository(TestRepository.class); + assertEquals(1, repositoryFactory.getCacheSize()); + + repositoryFactory.clearCache(); + assertEquals(0, repositoryFactory.getCacheSize()); + } + + @Test + public void testGetCacheSize() { + assertEquals(0, repositoryFactory.getCacheSize()); + + repositoryFactory.createRepository(TestRepository.class); + assertEquals(1, repositoryFactory.getCacheSize()); + + repositoryFactory.createRepository(AnotherTestRepository.class); + assertEquals(2, repositoryFactory.getCacheSize()); + } + + @Test + public void testCreateRepository_SessionFactoryNotAvailable() { + // Create factory with uninitialized HibernateConfig + when(hibernateConfig.isInitialized()).thenReturn(false); + RepositoryFactory uninitializedFactory = new RepositoryFactory(hibernateConfig); + uninitializedFactory.initialize(); + + assertThrows(IllegalStateException.class, () -> { + uninitializedFactory.createRepository(TestRepository.class); + }); + } + + @Test + public void testRepositoryProxy_MethodDelegation() { + TestRepository repository = repositoryFactory.createRepository(TestRepository.class); + + // Test that basic methods don't throw exceptions + assertDoesNotThrow(() -> { + repository.toString(); + repository.hashCode(); + }); + } + + // Test interfaces and entities + + public interface TestRepository extends BaseRepository { + // Custom repository methods can be added here + } + + public interface AnotherTestRepository extends BaseRepository { + } + + // Invalid repository interface (doesn't extend BaseRepository) + public interface InvalidRepository { + void someMethod(); + } + + @Entity + public static class TestEntity { + @Id + private Long id; + private String name; + + public TestEntity() {} + + public TestEntity(String name) { + this.name = name; + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + + @Entity + public static class AnotherTestEntity { + @Id + private Long id; + private String description; + + public AnotherTestEntity() {} + + public AnotherTestEntity(String description) { + this.description = description; + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + } +} \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..61371aa --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,25 @@ +# Test Configuration for JazzyFramework Database Tests + +# Database Configuration - H2 in-memory for tests +jazzy.datasource.url=jdbc:h2:mem:test_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +jazzy.datasource.username=sa +jazzy.datasource.password= +jazzy.datasource.driver-class-name=org.h2.Driver + +# JPA/Hibernate Configuration for Testing +jazzy.jpa.database-platform=org.hibernate.dialect.H2Dialect +jazzy.jpa.hibernate.ddl-auto=create-drop +jazzy.jpa.show-sql=false +jazzy.jpa.properties.hibernate.format_sql=false + +# H2 Console disabled for tests +jazzy.h2.console.enabled=false + +# Database enabled for tests +jazzy.database.enabled=true + +# Test-specific properties +test.property=test_value +test.number=42 +test.boolean.true=true +test.boolean.false=false \ No newline at end of file