Compare commits

..
Author SHA1 Message Date
Gustavo Henrique Santos Souza de Miranda f70c1e1d94 Switch from EntityManager to EntityManagerFactory across repositories, managers, and services for improved resource management and thread safety. 2025-11-29 03:02:49 -03:00
Gustavo Henrique Santos Souza de Miranda 32fff9c725 Add null-safe ID handling in GenreMapper and fix minor logging format in IPCManager
- Ensure `GenreMapper` sets ID only if it's non-null and valid (> 0) to prevent potential NPEs.
- Adjust logging format in `IPCManager` for consistent indentation.
2025-11-29 02:50:07 -03:00
Gustavo Henrique Santos Souza de Miranda 7b61b2071e Implement Genre Management with CRUD Operations
- Add `Genre` entity with JPA annotations and database mapping.
- Create repository, service, and delegate handlers for Genre CRUD operations.
- Register handlers (`create_genre`, `get_genres`, `get_genre_by_id`, `update_genre`, `delete_genre`) in `DelegateActionManager`.
- Introduce Protobuf definitions for Genre messages.
- Update `DatabaseManager` to expose `EntityManager`.
- Enhance `DelegateActionManager` constructor to use `EntityManager` for handler initialization.
2025-11-28 23:33:27 -03:00
Gustavo Henrique Miranda ba53d4d8c4 Merge pull request #10 from gmbrax/refactor/add-hibernate-and-reflection-dependency
Remove `Media` entity and integrate Hibernate ORM into `DatabaseManager`:
2025-11-28 00:38:21 -03:00
Gustavo Henrique Santos Souza de Miranda 081a1f1ed4 Add exception handling for entity scanning in DatabaseManager
- Wrap entity scanning in a try-catch block to handle potential exceptions.
- Log descriptive error messages and throw a runtime exception for failure cases.
- Warn if no `@Entity` classes are found during the scan.
2025-11-27 19:20:22 -03:00
Gustavo Henrique Santos Souza de Miranda e395e21e8c Add validation for required Hibernate configuration properties in DatabaseManager
- Ensure non-null and non-empty values for `hibernate.dialect`, `database.driver`, and `connectionUrl`.
- Throw `IllegalStateException` when required properties are missing.
2025-11-27 19:08:11 -03:00
Gustavo Henrique Santos Souza de Miranda 412a590a98 Add exception handling for Hibernate initialization in DatabaseManager
- Wrap Hibernate setup in a try-catch block.
- Log errors during initialization failure with descriptive messages.
- Throw a runtime exception if setup fails to ensure proper error propagation.
2025-11-27 19:04:44 -03:00
Gustavo Henrique Santos Souza de Miranda bc0199312f Add null and open-state checks for EntityManager and EntityManagerFactory closure
- Ensure safe closure of `EntityManager` and `EntityManagerFactory`.
- Add logging for successful closure and error handling during exceptions.
2025-11-27 18:55:43 -03:00
Gustavo Henrique Santos Souza de Miranda 4ae1e3075d Remove Media entity and integrate Hibernate ORM into DatabaseManager:
- Deleted `Media` class and its annotations.
- Added Hibernate ORM initialization in `DatabaseManager`, including dynamic entity scanning and configuration.
- Updated `pom.xml` to include dependencies for Hibernate community dialects and Reflections.
2025-11-27 18:40:08 -03:00
Gustavo Henrique Miranda fc09c4e257 Merge pull request #9 from gmbrax/fix/fix-unsecure-postgres-version
Fix the pom.xml
2025-11-24 22:19:31 -03:00
Gustavo Henrique Santos Souza de Miranda 53004a3730 Fix the pom.xml to alter the postgres dependency to a safer version due a CVE in the 42.7.5 then it was bumped t0 42.7.7 2025-11-24 21:59:35 -03:00
Gustavo Henrique Miranda 7e154eb01c Merge pull request #7 from gmbrax/feature/IPC-Unix-Socket
Feature/ipc unix socket
2025-11-22 23:41:12 -03:00
Gustavo Henrique Miranda 24fadf9474 Merge pull request #6 from gmbrax/feature/add-SQLite-Implementation
Feature/add sq lite implementation
2025-11-22 23:37:31 -03:00
Gustavo Henrique Santos Souza de Miranda 269780d8cf Add CloseHandler to support graceful connection termination.
- Extend `test.proto` with `CloseCommand` and `CloseResponse` messages.
- Introduce `CloseHandler` to process "close" actions and respond with connection termination notice.
- Update `DelegateActionManager` to register `CloseHandler`.
- Refactor `IPCManager` to handle "close" response headers and terminate client connections gracefully.
2025-11-22 23:27:04 -03:00
Gustavo Henrique Santos Souza de Miranda 6576b54057 - Add HeartbeatHandler and enhance EchoHandler for Protocol Buffers support.
- Extend `test.proto` schema with heartbeat and echo message definitions.
- Register `HeartbeatHandler` in `DelegateActionManager`.
- Refactor Log4j2 configuration to standardize log levels.
- Optimize client wait time in `IPCManager` for reduced latency.
2025-11-22 02:29:28 -03:00
Gustavo Henrique Santos Souza de Miranda 2704be31bc Fix logging library dependency in pom.xml: replace log4j-slf4j2-impl with log4j-slf4j-impl. 2025-11-21 18:19:38 -03:00
Gustavo Henrique Santos Souza de Miranda dc4b6cbdfa Add SQLite support with SqliteDatabaseManager and application mode handling:
- Created `SqliteDatabaseManager` for SQLite database initialization and management.
- Refactored `DatabaseManager` into an abstract class to support different database implementations.
- Introduced `ApplicationMode` enum to switch between `local` and `server` modes.
- Updated `MediaManagerApplication` to handle `runtype` configuration and initialize SQLite in `local` mode.
- Updated `config.properties.example` with new `runtype` property.
- Added SQLite JDBC dependency to `pom.xml`.
2025-11-21 18:03:20 -03:00
Gustavo Henrique Miranda 77259308c6 Merge pull request #5 from gmbrax/feature/IPC-Unix-Socket
Feature/ipc unix socket
2025-11-20 09:16:50 -03:00
Gustavo Henrique Santos Souza de Miranda a0ad10b1bc Integrate DelegateActionManager for request handling and refactor IPC:
- Replaced `SimpleProtocol` with `TransportProtocol` in `IPCManager`.
- Added `DelegateActionManager` to route requests to handlers, starting with `EchoHandler`.
- Updated `MediaManagerApplication` to initialize and manage `DelegateActionManager`.
- Extended Protocol Buffers schema with `Request` and `Response` definitions.
- Enhanced logging for request processing and redesigned response flow.
2025-11-17 07:21:52 -03:00
Gustavo Henrique Santos Souza de Miranda 23b6d54674 Add Protocol Buffers support to IPCManager and Maven build:
- Created `messages.proto` to define `TextMessage` schema.
- Integrated Protocol Buffers Java library into Maven dependencies.
- Configured Maven plugins for Protocol Buffers compilation and generated source management.
- Implemented message serialization and deserialization in `IPCManager`.
- Enhanced client handling to process and respond with Protocol Buffers messages.
2025-11-15 01:54:31 -03:00
Gustavo Henrique Miranda 3998b18671 Merge pull request #4 from gmbrax/feature/IPC-Unix-Socket
feature/IPC-Unix-Socket
2025-11-14 22:40:49 -03:00
Gustavo Henrique Santos Souza de Miranda 3854635185 Remove the unused checkUnixSocketExists method from IPCManager. 2025-11-14 19:52:53 -03:00
Gustavo Henrique Santos Souza de Miranda f1071dca03 Enhance IPCManager with non-blocking server support and improved connection handling:
- Configured UNIX domain socket for non-blocking mode to prevent indefinite blocking on `accept()`.
- Introduced connection loop with thread pool for client handling.
- Added `ClientHandler` to process client communications.
- Implemented proper resource cleanup during shutdown.
- Improved logging for socket initialization, client handling, and shutdown.
2025-11-14 06:22:35 -03:00
Gustavo Henrique Santos Souza de Miranda 73d1753935 Add IPCManager for UNIX domain socket communication:
- Introduced `IPCManager` class to handle IPC using UNIX domain sockets.
- Updated `MediaManagerApplication` to initialize and manage `IPCManager` during startup and shutdown.
- Enhanced logging for IPC initialization and cleanup.
- Updated `config.properties.example` with `ipc.socket.path` property.
2025-11-13 20:45:37 -03:00
Gustavo Henrique Miranda 81e38ce9ae Merge pull request #3 from gmbrax/fix/Fix-exir-log-messages
Improve logging, shutdown behavior, and build configuration:
2025-11-13 17:18:47 -03:00
Gustavo Henrique Santos Souza de Miranda b7ff9d1e16 Improve logging, shutdown behavior, and build configuration:
- Enabled `immediateFlush` for log appenders and disabled `shutdownHook` in Log4j2 configuration.
- Enhanced shutdown hook with graceful Log4j2 termination and extended delay for log flushing.
- Improved database connection closure logging in `DatabaseManager`.
- Integrated Maven Shade Plugin to create executable JARs with all dependencies.
2025-11-13 17:10:23 -03:00
Gustavo Henrique Santos Souza de Miranda b0c45bf657 Refactor code for clarity and consistent formatting:
- Added missing braces in the shutdown hook.
- Removed unnecessary empty lines.
- Streamlined `try` block indentation in `loadConfiguration()`.
2025-11-13 05:58:57 -03:00
Gustavo Henrique Miranda ed2f197c60 Merge pull request #2 from gmbrax/feature/Database-Connector
Feature/database connector
2025-11-13 05:52:25 -03:00
Gustavo Henrique Miranda 01f12c4e15 Merge branch 'develop' into feature/Database-Connector 2025-11-13 05:50:00 -03:00
Gustavo Henrique Miranda 8c291867ce Merge pull request #1 from gmbrax/fix/fix-continous-run
Improve shutdown hook and application lifecycle logging:
2025-11-13 05:47:47 -03:00
Gustavo Henrique Santos Souza de Miranda 65e1493007 Improve shutdown hook and application lifecycle logging:
- Added graceful shutdown messages.
- Handled `InterruptedException` to ensure proper shutdown behavior.
- Included clear prompts for application status.
2025-11-13 05:40:01 -03:00
Gustavo Henrique Santos Souza de Miranda 3bb0423167 Enhance DatabaseManager with logging, validation, and cleanup
- Added detailed logging for database initialization and connection handling.
- Implemented configuration validation and sanity checks during initialization.
- Introduced `close()` method to properly release database resources.
- Updated `MediaManagerApplication` to invoke `DatabaseManager.close()` on shutdown.
- Updated `.gitignore` to exclude sensitive configuration files while allowing example files.
2025-11-13 04:43:05 -03:00
Gustavo Henrique Santos Souza de Miranda 22476abb27 Add DatabaseManager class and initialize database connection
- Introduced `DatabaseManager` class to manage database connections.
- Updated `MediaManagerApplication` to initialize `DatabaseManager` using configuration properties.
2025-11-13 03:59:31 -03:00
Gustavo Henrique Santos Souza de Miranda 885d5c95a4 Replace application.properties with config.properties:
- Renamed configuration file to `config.properties` for consistency.
- Updated `MediaManagerApplication` to load the new configuration file.
- Adjusted references in `README.md` and `.gitignore`.
2025-11-13 03:25:33 -03:00
27 changed files with 1687 additions and 150 deletions
+9 -1
View File
@@ -39,8 +39,16 @@ build/
.DS_Store
### Application Specific ###
application-local.properties
config-local.properties
*.db
pipes/
*.log
/.idea/
Configuration files with sensitive credentials
# These files contain database passwords and API keys and should never be committed
src/main/resources/config.properties
src/main/resources/application.properties
# Allow example configuration files to be committed
!src/main/resources/*.properties.example
+5 -5
View File
@@ -34,7 +34,7 @@ MediaManager-Core/
│ │ ├── util/ # Utility classes
│ │ └── MediaManagerApplication.java
│ └── resources/
│ ├── application.properties # App configuration
│ ├── config.properties # App configuration
│ ├── log4j2.xml # Logging configuration
│ └── META-INF/
│ └── persistence.xml # JPA configuration
@@ -55,7 +55,7 @@ GRANT ALL PRIVILEGES ON DATABASE mediamanager TO mediamanager;
### 2. Configuration
Edit `src/main/resources/application.properties` and update the database credentials:
Edit `src/main/resources/config.properties` and update the database credentials:
```properties
db.url=jdbc:postgresql://localhost:5432/mediamanager
@@ -90,7 +90,7 @@ The application creates named pipes for inter-process communication. Default con
- Pipe name: `mediamanager-pipe`
- Buffer size: 8192 bytes
You can modify these settings in `application.properties`.
You can modify these settings in `config.properties`.
## Logging
@@ -116,8 +116,8 @@ mvn test
## Dependencies
- PostgreSQL Driver: 42.7.3
- Hibernate ORM: 6.4.4.Final
- PostgreSQL Driver: 42.7.5
- Hibernate ORM: 7.1.7.Final
- HikariCP: 5.1.0
- Log4j 2: 2.23.1
- Jackson: 2.16.1
+95 -2
View File
@@ -13,7 +13,7 @@
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hibernate.version>7.1.7.Final</hibernate.version>
<postgresql.version>42.7.5</postgresql.version>
<postgresql.version>42.7.7</postgresql.version>
<hikaricp.version>5.1.0</hikaricp.version>
<log4j.version>2.23.1</log4j.version>
<junit.version>5.10.2</junit.version>
@@ -35,6 +35,12 @@
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
<version>${hibernate.version}</version>
</dependency>
<!-- HikariCP for connection pooling -->
<dependency>
<groupId>com.zaxxer</groupId>
@@ -55,7 +61,7 @@
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
</dependency>
@@ -78,9 +84,32 @@
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.32.0</version>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.44.1.0</version>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -91,11 +120,75 @@
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<!-- Protocol Buffers Plugin -->
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocExecutable>/usr/bin/protoc</protocExecutable>
<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
<outputDirectory>${project.build.directory}/generated-sources/protobuf/java</outputDirectory>
<clearOutputDirectory>false</clearOutputDirectory>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Build Helper Plugin - Adiciona generated sources ao classpath -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/protobuf/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<!-- Maven Shade Plugin - Creates executable JAR with all dependencies -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.mediamanager.MediaManagerApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
@@ -1,5 +1,8 @@
package com.mediamanager;
import com.mediamanager.service.database.DatabaseManager;
import com.mediamanager.service.delegate.DelegateActionManager;
import com.mediamanager.service.ipc.IPCManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -7,9 +10,25 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import com.mediamanager.service.database.SqliteDatabaseManager;
public class MediaManagerApplication {
private static final Logger logger = LogManager.getLogger(MediaManagerApplication.class);
private static Properties config;
private static DatabaseManager databaseManager;
private static DelegateActionManager actionManager;
private static IPCManager ipcManager;
public enum ApplicationMode {
LOCAL("local"),
SERVER("server");
private final String value;
ApplicationMode(String value) {
this.value = value;
}
}
public static void main(String[] args) {
logger.info("Starting MediaManager Core Application...");
@@ -17,19 +36,85 @@ public class MediaManagerApplication {
try {
// Load configuration
loadConfiguration();
String runTypeString = config.getProperty("runtype","local");
ApplicationMode mode = null;
for (ApplicationMode am : ApplicationMode.values()) {
if (am.value.equalsIgnoreCase(runTypeString)) {
mode = am;
break;
}
}
if (mode == null) {
logger.error("Invalid run type: {}", runTypeString);
throw new Exception("Invalid run type: " + runTypeString);
}
logger.info("Run type: {}", mode);
switch (mode) {
case LOCAL:
logger.info("Starting local database...");
databaseManager = new SqliteDatabaseManager(config);
break;
case SERVER:
throw new Exception("Server mode not yet implemented");
default:
}
databaseManager.init();
actionManager = new DelegateActionManager(databaseManager.getEntityManagerFactory());
actionManager.start();
ipcManager = new IPCManager(config,actionManager);
ipcManager.init();
// TODO: Initialize database connection
// TODO: Initialize IPC server with named pipes
// TODO: Start application services
logger.info("MediaManager Core started successfully");
logger.info("IPC Pipe: {}", config.getProperty("ipc.pipe.path") + "/" + config.getProperty("ipc.pipe.name"));
logger.info("IPC Socket: {}", ipcManager.getSocketPath().toAbsolutePath().toString());
// Keep application running
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
logger.info("Shutting down MediaManager Core...");
// TODO: Cleanup resources
if (databaseManager != null) {
databaseManager.close();
}
if (ipcManager != null) {
try {
ipcManager.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
if (actionManager != null) {
actionManager.stop();
}
logger.info("MediaManager Core shutdown successfully");
logger.info("Goodbye!");
// Give Log4j2 time to write all pending messages before shutting down
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Now shutdown Log4j2
org.apache.logging.log4j.LogManager.shutdown();
}));
logger.info("Application is running");
logger.info("Press Ctrl+C to exit");
Thread.currentThread().join();
} catch (InterruptedException e) {
logger.info("Application interrupted, initiating shutdown...");
Thread.currentThread().interrupt();
} catch (Exception e) {
logger.error("Failed to start MediaManager Core", e);
@@ -39,10 +124,9 @@ public class MediaManagerApplication {
private static void loadConfiguration() throws IOException {
config = new Properties();
try (InputStream input = MediaManagerApplication.class.getClassLoader()
.getResourceAsStream("application.properties")) {
try (InputStream input = MediaManagerApplication.class.getClassLoader().getResourceAsStream("config.properties")) {
if (input == null) {
throw new IOException("Unable to find application.properties");
throw new IOException("Unable to find config.properties");
}
config.load(input);
logger.info("Configuration loaded successfully");
@@ -0,0 +1,40 @@
package com.mediamanager.mapper;
import com.mediamanager.model.Genre;
import com.mediamanager.protocol.messages.GenreMessages;
public class GenreMapper {
public static GenreMessages.Genre toProtobuf(Genre entity) {
if (entity == null) {
return null;
}
GenreMessages.Genre.Builder builder = GenreMessages.Genre.newBuilder()
.setName(entity.getName());
// Only set ID when it's present and valid (> 0). Avoids NPE for null IDs.
Integer id = entity.getId();
if (id != null && id > 0) {
builder.setId(id);
}
return builder.build();
}
public static Genre toEntity(GenreMessages.Genre protobuf) {
if (protobuf == null) {
return null;
}
Genre entity = new Genre();
// Só seta ID se for > 0 (protobuf default é 0)
if (protobuf.getId() > 0) {
entity.setId(protobuf.getId());
}
entity.setName(protobuf.getName());
return entity;
}
}
@@ -0,0 +1,30 @@
package com.mediamanager.model;
import jakarta.persistence.*;
@Entity
@Table(name = "genre")
public class Genre {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false)
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -1,128 +0,0 @@
package com.mediamanager.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "media")
public class Media {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false, unique = true)
private String filePath;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private MediaType type;
@Column
private Long fileSize;
@Column
private String mimeType;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Constructors
public Media() {}
public Media(String title, String filePath, MediaType type) {
this.title = title;
this.filePath = filePath;
this.type = type;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public MediaType getType() {
return type;
}
public void setType(MediaType type) {
this.type = type;
}
public Long getFileSize() {
return fileSize;
}
public void setFileSize(Long fileSize) {
this.fileSize = fileSize;
}
public String getMimeType() {
return mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
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;
}
public enum MediaType {
VIDEO,
AUDIO,
IMAGE,
DOCUMENT,
OTHER
}
}
@@ -0,0 +1,121 @@
package com.mediamanager.repository;
import com.mediamanager.model.Genre;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
import java.util.Optional;
/**
* Repository para acesso a dados de Genre
* Encapsula todas as operações de banco de dados
*/
public class GenreRepository {
private static final Logger logger = LogManager.getLogger(GenreRepository.class);
private final EntityManagerFactory entityManagerFactory;
public GenreRepository(EntityManagerFactory entityManagerFactory) {
this.entityManagerFactory = entityManagerFactory;
}
/**
* Salva um novo genre
*/
public Genre save(Genre genre) {
logger.debug("Saving genre: {}", genre.getName());
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
try {
em.persist(genre);
em.getTransaction().commit();
logger.debug("Genre saved with ID: {}", genre.getId());
return genre;
} catch (Exception e) {
em.getTransaction().rollback();
logger.error("Error saving genre", e);
throw e;
} finally {
if (em.isOpen()) em.close();
}
}
/**
* Busca todos os genres
*/
public List<Genre> findAll() {
logger.debug("Finding all genres");
EntityManager em = entityManagerFactory.createEntityManager();
try {
return em
.createQuery("SELECT g FROM Genre g ORDER BY g.name", Genre.class)
.getResultList();
} finally {
if (em.isOpen()) em.close();
}
}
/**
* Busca genre por ID
*/
public Optional<Genre> findById(Integer id) {
logger.debug("Finding genre by ID: {}", id);
EntityManager em = entityManagerFactory.createEntityManager();
try {
Genre genre = em.find(Genre.class, id);
return Optional.ofNullable(genre);
} finally {
if (em.isOpen()) em.close();
}
}
/**
* Atualiza um genre existente
*/
public Genre update(Genre genre) {
logger.debug("Updating genre ID: {}", genre.getId());
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
try {
Genre updated = em.merge(genre);
em.getTransaction().commit();
logger.debug("Genre updated successfully");
return updated;
} catch (Exception e) {
em.getTransaction().rollback();
logger.error("Error updating genre", e);
throw e;
} finally {
if (em.isOpen()) em.close();
}
}
/**
* Deleta um genre por ID
*/
public boolean deleteById(Integer id) {
logger.debug("Deleting genre by ID: {}", id);
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
try {
Genre genre = em.find(Genre.class, id);
if (genre == null) {
em.getTransaction().rollback();
return false;
}
em.remove(genre);
em.getTransaction().commit();
logger.debug("Genre deleted successfully");
return true;
} catch (Exception e) {
em.getTransaction().rollback();
logger.error("Error deleting genre", e);
throw e;
} finally {
if (em.isOpen()) em.close();
}
}
}
@@ -0,0 +1,142 @@
package com.mediamanager.service.database;
import jakarta.persistence.EntityManagerFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.hibernate.cfg.Configuration;
import org.reflections.Reflections;
import org.reflections.scanners.Scanners;
import jakarta.persistence.Entity;
import java.util.Set;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public abstract class DatabaseManager {
protected final Properties configuration;
protected Connection connection;
protected String connectionUrl;
protected EntityManagerFactory entityManagerFactory;
protected static final Logger logger = LogManager.getLogger(DatabaseManager.class);
public DatabaseManager(Properties config) {
this.configuration = config;
logger.debug("DatabaseManager created with configuration:");
}
public abstract void init() throws Exception;
protected abstract Connection createConnection() throws Exception;
protected void performSanityChecks() throws SQLException {
logger.debug("Performing sanity checks...");
try (Statement stmt = connection.createStatement()) {
stmt.execute("SELECT 1");
}
String databaseProductName = connection.getMetaData().getDatabaseProductName();
String databaseProductVersion = connection.getMetaData().getDatabaseProductVersion();
logger.info("Connected to database: {} v{}", databaseProductName, databaseProductVersion);
}
public Connection getConnection() {
return connection;
}
public void close() {
if (entityManagerFactory != null && entityManagerFactory.isOpen()) {
try {
entityManagerFactory.close();
logger.info("EntityManagerFactory closed");
} catch (Exception e) {
logger.error("Error closing EntityManagerFactory: {}", e.getMessage());
}
}
if (connection != null) {
try {
logger.info("Closing database connection...");
connection.close();
logger.info("Database connection closed successfully");
} catch (SQLException e) {
logger.error("Error closing database connection: {}", e.getMessage());
}
} else {
logger.debug("No database connection to close");
}
}
protected boolean testConnection() {
try (Statement stmt = connection.createStatement()) {
stmt.execute("SELECT 1");
return true;
} catch (SQLException e) {
logger.error("Connection test failed", e);
return false;
}
}
protected void initializeHibernate() {
logger.info("Initializing Hibernate ORM...");
Configuration hibernateConfig = new Configuration();
String dialect = configuration.getProperty("hibernate.dialect");
String hbm2ddl = configuration.getProperty("hibernate.hbm2ddl.auto");
String driver = configuration.getProperty("database.driver");
if (dialect == null || dialect.isEmpty()) {
throw new IllegalStateException("hibernate.dialect property is required but not configured");
}
if (driver == null || driver.isEmpty()) {
throw new IllegalStateException("database.driver property is required but not configured");
}
if (connectionUrl == null || connectionUrl.isEmpty()) {
throw new IllegalStateException("connectionUrl must be set before initializing Hibernate");
}
hibernateConfig.setProperty("hibernate.connection.url", connectionUrl);
hibernateConfig.setProperty("hibernate.connection.driver_class", driver);
hibernateConfig.setProperty("hibernate.dialect", dialect);
hibernateConfig.setProperty("hibernate.hbm2ddl.auto", hbm2ddl);
hibernateConfig.setProperty("hibernate.show_sql",
configuration.getProperty("hibernate.show_sql", "false"));
hibernateConfig.setProperty("hibernate.format_sql",
configuration.getProperty("hibernate.format_sql", "true"));
logger.info("Scanning for entities in package: com.mediamanager.model");
Set<Class<?>> entityClasses;
try {
Reflections reflections = new Reflections("com.mediamanager.model", Scanners.TypesAnnotated);
entityClasses = reflections.getTypesAnnotatedWith(Entity.class);
} catch (Exception e) {
logger.error("Failed to scan for entities: {}", e.getMessage());
throw new RuntimeException("Entity scanning failed", e);
}
logger.info("Found {} entities", entityClasses.size());
if (entityClasses.isEmpty()) {
logger.warn("No @Entity classes found in package com.mediamanager.model - is this expected?");
}
for (Class<?> entityClass : entityClasses) {
logger.debug("Registering entity: {}", entityClass.getSimpleName());
hibernateConfig.addAnnotatedClass(entityClass);
}
try {
entityManagerFactory = hibernateConfig.buildSessionFactory().unwrap(EntityManagerFactory.class);
} catch (Exception e) {
logger.error("Failed to initialize Hibernate: {}", e.getMessage());
throw new RuntimeException("Hibernate initialization failed", e);
}
logger.info("Hibernate ORM initialized successfully");
}
public EntityManagerFactory getEntityManagerFactory() {
return entityManagerFactory;
}
}
@@ -0,0 +1,99 @@
package com.mediamanager.service.database;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public class SqliteDatabaseManager extends DatabaseManager {
private static final Logger logger = LogManager.getLogger(SqliteDatabaseManager.class);
public SqliteDatabaseManager(Properties config) {
super(config);
}
@Override
public void init() throws Exception {
logger.info("Initializing SQLite database...");
String dataDir = configuration.getProperty("database.dir",
System.getProperty("user.home") + "/.mediamanager/db");
String dbFilename = configuration.getProperty("database.filename", "mediamanager.db");
String driverClassName = configuration.getProperty("database.driver", "org.sqlite.JDBC");
try {
Class.forName(driverClassName);
}catch(ClassNotFoundException e) {
logger.error("Failed to load SQLite driver", e);
throw e;
}
Path dataPath = Paths.get(dataDir);
if (!Files.exists(dataPath)) {
Files.createDirectories(dataPath);
logger.debug("Created database directory: {}", dataDir);
}
Path dbFile = dataPath.resolve(dbFilename);
this.connectionUrl = "jdbc:sqlite:" + dbFile.toAbsolutePath().toString();
logger.info("Database file path: {}", dbFile);
logger.info("Connection URL: {}", this.connectionUrl);
initializeHibernate();
this.connection = createConnection();
configurePerformancePragmas();
performSanityChecks();
ensureSchemaExists();
logger.info("SQLite database initialized successfully");
}
@Override
protected Connection createConnection() throws Exception {
try {
// O driver org.xerial.sqlite-jdbc é carregado automaticamente aqui
Connection conn = DriverManager.getConnection(this.connectionUrl);
logger.debug("Got connection to SQLite file");
return conn;
} catch (SQLException e) {
logger.error("Failed to create SQLite connection", e);
throw new Exception("SQLite connection failed: " + e.getMessage(), e);
}
}
private void configurePerformancePragmas() throws SQLException {
try (Statement stmt = connection.createStatement()) {
stmt.execute("PRAGMA journal_mode=WAL;");
stmt.execute("PRAGMA foreign_keys=ON;");
stmt.execute("PRAGMA synchronous=NORMAL;");
stmt.execute("PRAGMA busy_timeout=5000;");
logger.debug("SQLite performance PRAGMAs applied (WAL, Synchronous, ForeignKeys).");
}
}
private void ensureSchemaExists() throws SQLException {
}
@Override
public void close() {
super.close();
logger.info("SQLite resources released.");
}
}
@@ -0,0 +1,11 @@
package com.mediamanager.service.delegate;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mediamanager.protocol.TransportProtocol;
@FunctionalInterface
public interface ActionHandler {
TransportProtocol.Response.Builder handle(ByteString requestPayload) throws InvalidProtocolBufferException;
}
@@ -0,0 +1,78 @@
package com.mediamanager.service.delegate;
import com.google.protobuf.ByteString;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.repository.GenreRepository;
import com.mediamanager.service.delegate.handler.CloseHandler;
import com.mediamanager.service.delegate.handler.EchoHandler;
import com.mediamanager.service.delegate.handler.HeartbeatHandler;
import com.mediamanager.service.delegate.handler.genre.*;
import com.mediamanager.service.genre.GenreService;
import jakarta.persistence.EntityManagerFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.HashMap;
import java.util.Map;
public class DelegateActionManager {
private static final Logger logger = LogManager.getLogger(DelegateActionManager.class);
private final Map<String, ActionHandler> handlerRegistry;
private final EntityManagerFactory entityManagerFactory;
private final GenreService genreService;
public DelegateActionManager(EntityManagerFactory entityManagerFactory) {
this.entityManagerFactory = entityManagerFactory;
GenreRepository genreRepository = new GenreRepository(entityManagerFactory);
this.genreService = new GenreService(genreRepository);
logger.debug("DelegateActionManager created");
this.handlerRegistry = new HashMap<>();
registerHandlers();
}
private void registerHandlers() {
handlerRegistry.put("echo",new EchoHandler());
handlerRegistry.put("heartbeat",new HeartbeatHandler());
handlerRegistry.put("close", new CloseHandler());
handlerRegistry.put("create_genre", new CreateGenreHandler(genreService));
handlerRegistry.put("get_genres", new GetGenreHandler(genreService));
handlerRegistry.put("get_genre_by_id", new GetGenreByIdHandler(genreService));
handlerRegistry.put("update_genre", new UpdateGenreHandler(genreService));
handlerRegistry.put("delete_genre", new DeleteGenreHandler(genreService));
}
public void start(){
logger.info("DelegateActionManager started");
}
public void stop(){
logger.info("DelegateActionManager stopped");
}
public TransportProtocol.Response ProcessedRequest(TransportProtocol.Request request){
String requestId = request.getRequestId();
logger.info("Processing request: {}", requestId);
String action = request.getHeadersMap().getOrDefault("action", "unknown");
ActionHandler handler = handlerRegistry.get(action);
TransportProtocol.Response.Builder responseBuilder;
if (handler == null) {
logger.warn("No handler found for action: {}", action);
responseBuilder = TransportProtocol.Response.newBuilder()
.setStatusCode(404) // 404 Not Found
.setPayload(ByteString.copyFromUtf8("Error: Action '" + action + "' not found."));
} else{
try {
logger.debug("Delegating action '{}' to handler...", action);
responseBuilder = handler.handle(request.getPayload());
}catch (Exception e) {
logger.error("Handler for action '{}' threw an exception:", action, e);
responseBuilder = TransportProtocol.Response.newBuilder()
.setStatusCode(500) // 500 Internal Server Error
.setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage()));
}
}
return responseBuilder.setRequestId(requestId).build();
}
}
@@ -0,0 +1,32 @@
package com.mediamanager.service.delegate.handler;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mediamanager.protocol.TestProtocol.CloseCommand;
import com.mediamanager.protocol.TestProtocol.CloseResponse;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.service.delegate.ActionHandler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class CloseHandler implements ActionHandler {
private static final Logger logger = LogManager.getLogger(CloseHandler.class);
@Override
public TransportProtocol.Response.Builder handle(ByteString requestPayload)
throws InvalidProtocolBufferException {
CloseCommand.parseFrom(requestPayload); // Valida
logger.info("Close command received - connection will close");
CloseResponse response = CloseResponse.newBuilder()
.setMessage("Connection closing. Goodbye!")
.build();
return TransportProtocol.Response.newBuilder()
.setPayload(ByteString.copyFrom(response.toByteArray()))
.setStatusCode(200)
.putHeaders("Connection", "close"); // ← Marca para fechar
}
}
@@ -0,0 +1,39 @@
package com.mediamanager.service.delegate.handler;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mediamanager.protocol.TestProtocol.EchoCommand; // ← Import
import com.mediamanager.protocol.TestProtocol.EchoResponse; // ← Import
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.service.delegate.ActionHandler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class EchoHandler implements ActionHandler {
private static final Logger logger = LogManager.getLogger(EchoHandler.class);
@Override
public TransportProtocol.Response.Builder handle(ByteString requestPayload)
throws InvalidProtocolBufferException { // ← Pode lançar exceção
// 1. Parse Protobuf bytes → EchoCommand
EchoCommand command = EchoCommand.parseFrom(requestPayload);
logger.debug("Echo received: {}", command.getMessage());
// 2. Cria EchoResponse (Protobuf)
EchoResponse echoResponse = EchoResponse.newBuilder()
.setMessage(command.getMessage())
.setServerTimestamp(System.currentTimeMillis())
.build();
// 3. Serializa EchoResponse → bytes
ByteString responsePayload = ByteString.copyFrom(echoResponse.toByteArray());
// 4. Retorna Response
return TransportProtocol.Response.newBuilder()
.setPayload(responsePayload)
.setStatusCode(200);
}
}
@@ -0,0 +1,36 @@
package com.mediamanager.service.delegate.handler;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mediamanager.protocol.TestProtocol.HeartbeatCommand;
import com.mediamanager.protocol.TestProtocol.HeartbeatResponse;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.service.delegate.ActionHandler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class HeartbeatHandler implements ActionHandler {
private static final Logger logger = LogManager.getLogger(HeartbeatHandler.class);
@Override
public TransportProtocol.Response.Builder handle(ByteString requestPayload)
throws InvalidProtocolBufferException {
HeartbeatCommand command = HeartbeatCommand.parseFrom(requestPayload);
long serverTime = System.currentTimeMillis();
logger.debug("Heartbeat received. Client T1={}, Server T2={}",
command.getClientTimestamp(), serverTime);
HeartbeatResponse response = HeartbeatResponse.newBuilder()
.setClientTimestamp(command.getClientTimestamp()) // Echo T1
.setServerTimestamp(serverTime) // T2
.build();
return TransportProtocol.Response.newBuilder()
.setPayload(ByteString.copyFrom(response.toByteArray()))
.setStatusCode(200);
}
}
@@ -0,0 +1,60 @@
package com.mediamanager.service.delegate.handler.genre;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mediamanager.mapper.GenreMapper;
import com.mediamanager.model.Genre;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.protocol.messages.GenreMessages;
import com.mediamanager.service.delegate.ActionHandler;
import com.mediamanager.service.genre.GenreService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class CreateGenreHandler implements ActionHandler {
private static final Logger logger = LogManager.getLogger(CreateGenreHandler.class);
private final GenreService genreService;
public CreateGenreHandler(GenreService genreService) {
this.genreService = genreService;
}
@Override
public TransportProtocol.Response.Builder handle(ByteString requestPayload)
throws InvalidProtocolBufferException {
try {
// 1. Parse protobuf request
GenreMessages.CreateGenreRequest createRequest =
GenreMessages.CreateGenreRequest.parseFrom(requestPayload);
// 2. Chama service (lógica de negócio)
Genre genre = genreService.createGenre(createRequest.getName());
// 3. Converte entity para protobuf
GenreMessages.Genre genreProto = GenreMapper.toProtobuf(genre);
// 4. Cria response protobuf
GenreMessages.CreateGenreResponse createResponse =
GenreMessages.CreateGenreResponse.newBuilder()
.setGenre(genreProto)
.build();
// 5. Retorna
return TransportProtocol.Response.newBuilder()
.setPayload(createResponse.toByteString());
} catch (IllegalArgumentException e) {
logger.error("Validation error", e);
return TransportProtocol.Response.newBuilder()
.setStatusCode(400)
.setPayload(ByteString.copyFromUtf8("Validation error: " + e.getMessage()));
} catch (Exception e) {
logger.error("Error creating genre", e);
return TransportProtocol.Response.newBuilder()
.setStatusCode(500)
.setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage()));
}
}
}
@@ -0,0 +1,70 @@
package com.mediamanager.service.delegate.handler.genre;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.protocol.messages.GenreMessages;
import com.mediamanager.service.delegate.ActionHandler;
import com.mediamanager.service.genre.GenreService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class DeleteGenreHandler implements ActionHandler {
private static final Logger logger = LogManager.getLogger(DeleteGenreHandler.class);
private final GenreService genreService;
public DeleteGenreHandler(GenreService genreService) {
this.genreService = genreService;
}
@Override
public TransportProtocol.Response.Builder handle(ByteString requestPayload)
throws InvalidProtocolBufferException {
try {
// 1. Parse protobuf request
GenreMessages.DeleteGenreRequest deleteRequest =
GenreMessages.DeleteGenreRequest.parseFrom(requestPayload);
int id = deleteRequest.getId();
// 2. Deleta via service
boolean success = genreService.deleteGenre(id);
// 3. Cria response
GenreMessages.DeleteGenreResponse deleteResponse;
if (success) {
deleteResponse = GenreMessages.DeleteGenreResponse.newBuilder()
.setSuccess(true)
.setMessage("Genre deleted successfully")
.build();
return TransportProtocol.Response.newBuilder()
.setPayload(deleteResponse.toByteString());
} else {
deleteResponse = GenreMessages.DeleteGenreResponse.newBuilder()
.setSuccess(false)
.setMessage("Genre not found")
.build();
return TransportProtocol.Response.newBuilder()
.setStatusCode(404)
.setPayload(deleteResponse.toByteString());
}
} catch (Exception e) {
logger.error("Error deleting genre", e);
GenreMessages.DeleteGenreResponse deleteResponse =
GenreMessages.DeleteGenreResponse.newBuilder()
.setSuccess(false)
.setMessage("Error: " + e.getMessage())
.build();
return TransportProtocol.Response.newBuilder()
.setStatusCode(500)
.setPayload(deleteResponse.toByteString());
}
}
}
@@ -0,0 +1,64 @@
package com.mediamanager.service.delegate.handler.genre;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mediamanager.mapper.GenreMapper;
import com.mediamanager.model.Genre;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.protocol.messages.GenreMessages;
import com.mediamanager.service.delegate.ActionHandler;
import com.mediamanager.service.genre.GenreService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Optional;
public class GetGenreByIdHandler implements ActionHandler {
private static final Logger logger = LogManager.getLogger(GetGenreByIdHandler.class);
private final GenreService genreService;
public GetGenreByIdHandler(GenreService genreService) {
this.genreService = genreService;
}
@Override
public TransportProtocol.Response.Builder handle(ByteString requestPayload)
throws InvalidProtocolBufferException {
try {
// 1. Parse protobuf request
GenreMessages.GetGenreByIdRequest getByIdRequest =
GenreMessages.GetGenreByIdRequest.parseFrom(requestPayload);
int id = getByIdRequest.getId();
// 2. Busca via service
Optional<Genre> genreOpt = genreService.getGenreById(id);
if (genreOpt.isEmpty()) {
logger.warn("Genre not found with ID: {}", id);
return TransportProtocol.Response.newBuilder()
.setStatusCode(404)
.setPayload(ByteString.copyFromUtf8("Genre not found"));
}
// 3. Converte para protobuf
GenreMessages.Genre genreProto = GenreMapper.toProtobuf(genreOpt.get());
GenreMessages.GetGenreByIdResponse getByIdResponse =
GenreMessages.GetGenreByIdResponse.newBuilder()
.setGenre(genreProto)
.build();
// 4. Retorna
return TransportProtocol.Response.newBuilder()
.setPayload(getByIdResponse.toByteString());
} catch (Exception e) {
logger.error("Error getting genre by ID", e);
return TransportProtocol.Response.newBuilder()
.setStatusCode(500)
.setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage()));
}
}
}
@@ -0,0 +1,54 @@
package com.mediamanager.service.delegate.handler.genre;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mediamanager.mapper.GenreMapper;
import com.mediamanager.model.Genre;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.protocol.messages.GenreMessages;
import com.mediamanager.service.delegate.ActionHandler;
import com.mediamanager.service.genre.GenreService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
public class GetGenreHandler implements ActionHandler {
private static final Logger logger = LogManager.getLogger(GetGenreHandler.class);
private final GenreService genreService;
public GetGenreHandler(GenreService genreService) {
this.genreService = genreService;
}
@Override
public TransportProtocol.Response.Builder handle(ByteString requestPayload)
throws InvalidProtocolBufferException {
try {
// 1. Busca todos os genres via service
List<Genre> genres = genreService.getAllGenres();
// 2. Converte cada Genre para protobuf
GenreMessages.GetGenresResponse.Builder responseBuilder =
GenreMessages.GetGenresResponse.newBuilder();
for (Genre genre : genres) {
GenreMessages.Genre genreProto = GenreMapper.toProtobuf(genre);
responseBuilder.addGenres(genreProto);
}
GenreMessages.GetGenresResponse getGenresResponse = responseBuilder.build();
// 3. Retorna
return TransportProtocol.Response.newBuilder()
.setPayload(getGenresResponse.toByteString());
} catch (Exception e) {
logger.error("Error getting genres", e);
return TransportProtocol.Response.newBuilder()
.setStatusCode(500)
.setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage()));
}
}
}
@@ -0,0 +1,71 @@
package com.mediamanager.service.delegate.handler.genre;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mediamanager.mapper.GenreMapper;
import com.mediamanager.model.Genre;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.protocol.messages.GenreMessages;
import com.mediamanager.service.delegate.ActionHandler;
import com.mediamanager.service.genre.GenreService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Optional;
public class UpdateGenreHandler implements ActionHandler {
private static final Logger logger = LogManager.getLogger(UpdateGenreHandler.class);
private final GenreService genreService;
public UpdateGenreHandler(GenreService genreService) {
this.genreService = genreService;
}
@Override
public TransportProtocol.Response.Builder handle(ByteString requestPayload)
throws InvalidProtocolBufferException {
try {
// 1. Parse protobuf request
GenreMessages.UpdateGenreRequest updateRequest =
GenreMessages.UpdateGenreRequest.parseFrom(requestPayload);
int id = updateRequest.getId();
String newName = updateRequest.getName();
// 2. Atualiza via service
Optional<Genre> genreOpt = genreService.updateGenre(id, newName);
if (genreOpt.isEmpty()) {
logger.warn("Genre not found with ID: {}", id);
return TransportProtocol.Response.newBuilder()
.setStatusCode(404)
.setPayload(ByteString.copyFromUtf8("Genre not found"));
}
// 3. Converte para protobuf
GenreMessages.Genre genreProto = GenreMapper.toProtobuf(genreOpt.get());
GenreMessages.UpdateGenreResponse updateResponse =
GenreMessages.UpdateGenreResponse.newBuilder()
.setGenre(genreProto)
.build();
// 4. Retorna
return TransportProtocol.Response.newBuilder()
.setPayload(updateResponse.toByteString());
} catch (IllegalArgumentException e) {
logger.error("Validation error", e);
return TransportProtocol.Response.newBuilder()
.setStatusCode(400)
.setPayload(ByteString.copyFromUtf8("Validation error: " + e.getMessage()));
} catch (Exception e) {
logger.error("Error updating genre", e);
return TransportProtocol.Response.newBuilder()
.setStatusCode(500)
.setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage()));
}
}
}
@@ -0,0 +1,88 @@
package com.mediamanager.service.genre;
import com.mediamanager.model.Genre;
import com.mediamanager.repository.GenreRepository;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
import java.util.Optional;
/**
* Service para lógica de negócio relacionada a Genre
*/
public class GenreService {
private static final Logger logger = LogManager.getLogger(GenreService.class);
private final GenreRepository genreRepository;
public GenreService(GenreRepository genreRepository) {
this.genreRepository = genreRepository;
}
/**
* Cria um novo genre
*/
public Genre createGenre(String name) {
logger.info("Creating genre: {}", name);
// Aqui poderia ter validações de negócio
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Genre name cannot be empty");
}
Genre genre = new Genre();
genre.setName(name.trim());
return genreRepository.save(genre);
}
/**
* Busca todos os genres
*/
public List<Genre> getAllGenres() {
logger.info("Getting all genres");
return genreRepository.findAll();
}
/**
* Busca genre por ID
*/
public Optional<Genre> getGenreById(Integer id) {
logger.info("Getting genre by ID: {}", id);
return genreRepository.findById(id);
}
/**
* Atualiza um genre existente
*/
public Optional<Genre> updateGenre(Integer id, String name) {
logger.info("Updating genre ID {}: {}", id, name);
// Validação
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Genre name cannot be empty");
}
Optional<Genre> existingGenre = genreRepository.findById(id);
if (existingGenre.isEmpty()) {
logger.warn("Genre not found with ID: {}", id);
return Optional.empty();
}
Genre genre = existingGenre.get();
genre.setName(name.trim());
Genre updated = genreRepository.update(genre);
return Optional.of(updated);
}
/**
* Deleta um genre por ID
*/
public boolean deleteGenre(Integer id) {
logger.info("Deleting genre ID: {}", id);
return genreRepository.deleteById(id);
}
}
@@ -0,0 +1,337 @@
package com.mediamanager.service.ipc;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.service.delegate.DelegateActionManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.net.StandardProtocolFamily;
import java.net.UnixDomainSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class IPCManager {
private final Properties configuration;
private static final Logger logger = LogManager.getLogger(IPCManager.class);
private final DelegateActionManager actionManager;
private Path socketPath;
private UnixDomainSocketAddress socketAddress;
private ServerSocketChannel serverChannel;
private ExecutorService clientThreadPool;
private final AtomicBoolean running = new AtomicBoolean(false);
private final ConcurrentHashMap<Integer, ClientHandler> activeClients = new ConcurrentHashMap<>();
private final AtomicInteger clientIdCounter = new AtomicInteger(0);
public IPCManager(Properties config, DelegateActionManager actionManager){
configuration = config;
this.actionManager = actionManager;
logger.debug("IPCManager created with configuration:");
}
public void init() throws Exception {
logger.info("Initializing IPC connection...");
validateConfiguration();
socketPath = Path.of(configuration.getProperty("ipc.socket.path")).resolve("mediamanager.sock");
if (Files.exists(socketPath)) {
logger.warn("Socket file already exists at: {}", socketPath);
logger.info("Deleting existing socket...");
Files.deleteIfExists(socketPath);
}
Path parentDir = socketPath.getParent();
if (parentDir != null && !Files.exists(parentDir)) {
logger.info("Creating parent directory for socket: {}", parentDir);
Files.createDirectories(parentDir);
}
try {
socketAddress = UnixDomainSocketAddress.of(socketPath);
logger.debug("Socket address created");
serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
serverChannel.bind(socketAddress);
logger.info("Server bound to socket - file created at: {}", socketPath);
// ESTA É A MUDANÇA CRÍTICA
// Configura o canal para modo não-bloqueante
// Isso faz accept() retornar null imediatamente se não houver cliente
// ao invés de bloquear esperando indefinidamente
serverChannel.configureBlocking(false);
logger.debug("Server channel configured for non-blocking mode");
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------");
Files.setPosixFilePermissions(socketPath, perms);
logger.debug("Socket permissions set to: rw-------");
clientThreadPool = Executors.newCachedThreadPool(runnable -> {
Thread thread = new Thread(runnable);
thread.setName("IPC-Client-Handler-" + thread.getId());
thread.setDaemon(true);
return thread;
});
logger.debug("Client thread pool created");
running.set(true);
Thread serverThread = new Thread(this::acceptConnectionsLoop, "IPC-Server-Accept-Thread");
serverThread.setDaemon(true);
serverThread.start();
logger.info("Server thread started - accepting connections");
logger.info("IPC server initialized successfully on {}", socketPath.toAbsolutePath());
} catch (IOException e) {
logger.error("Failed to initialize IPC server: {}", e.getMessage());
throw new Exception("Failed to initialize IPC server: " + e.getMessage(), e);
}
}
private void validateConfiguration() throws Exception {
String[] requiredProperties = {
"ipc.socket.path"
};
for (String property : requiredProperties) {
if (configuration.getProperty(property) == null) {
throw new Exception("Missing required configuration property: " + property);
}
}
logger.debug("IPC configuration validated successfully");
}
public Path getSocketPath(){
return socketPath;
}
public void close() throws Exception {
if(!running.get()){
logger.warn("IPC connection is already closed");
}
logger.info("Closing IPC connection...");
running.set(false);
if (serverChannel != null && serverChannel.isOpen()) {
serverChannel.close();
logger.debug("Server channel closed");
}
if (clientThreadPool != null) {
clientThreadPool.shutdown();
try {
if (!clientThreadPool.awaitTermination(30, TimeUnit.SECONDS)) {
logger.warn("Some client handlers did not finish in time, forcing shutdown");
clientThreadPool.shutdownNow();
}
} catch (InterruptedException e) {
logger.error("Interrupted while waiting for client handlers", e);
clientThreadPool.shutdownNow();
Thread.currentThread().interrupt();
}
}
if (socketPath != null && Files.exists(socketPath)) {
Files.deleteIfExists(socketPath);
logger.info("Socket file deleted successfully");
}
logger.info("IPC server closed successfully");
}
private void acceptConnectionsLoop() {
logger.info("Preparing to accept connections...");
while (running.get()) {
try {
// Em modo não-bloqueante, accept() retorna imediatamente
// Se há um cliente esperando, retorna o SocketChannel
// Se não há cliente, retorna null
SocketChannel clientChannel = serverChannel.accept();
if (clientChannel != null) {
// Um cliente realmente se conectou!
int clientId = clientIdCounter.incrementAndGet();
logger.info("Client {} connected", clientId);
ClientHandler handler = new ClientHandler(clientId, clientChannel);
activeClients.put(clientId, handler);
clientThreadPool.submit(() -> {
try {
handler.handle();
} finally {
activeClients.remove(clientId);
logger.info("Client {} disconnected", clientId);
}
});
} else {
// Nenhum cliente conectado no momento
// Dorme por um curto período antes de verificar novamente
// Isso evita consumir CPU desnecessariamente em um loop vazio
Thread.sleep(1); // 1 milissegundos
}
} catch (InterruptedException e) {
// Thread foi interrompida, provavelmente durante shutdown
logger.debug("Accept loop interrupted");
break;
} catch (IOException e) {
// Erros de I/O reais devem ser logados
if (running.get()) {
logger.error("Error accepting client connection", e);
}
break;
}
}
logger.info("Connection loop stopped gracefully");
}
private class ClientHandler {
private final int clientId;
private final SocketChannel channel;
public ClientHandler(int clientId, SocketChannel channel) {
this.clientId = clientId;
this.channel = channel;
}
/**
* Método principal que processa a comunicação com o cliente.
* Aqui é onde vamos ler mensagens JSON, processá-las, e enviar respostas.
*/
public void handle() {
logger.debug("Client {} handler thread started", clientId);
try {
// LOOP: processa múltiplas requests na mesma conexão
while (channel.isOpen()) {
TransportProtocol.Request request = readRequest(channel);
if (request == null) {
// Cliente desconectou gracefully
logger.info("Client {} disconnected (end of stream)", clientId);
break;
}
logger.info("Client {} sent request {}", clientId, request.getRequestId());
// Processa usando o DelegateActionManager
TransportProtocol.Response response = actionManager.ProcessedRequest(request);
// Envia resposta de volta
writeResponse(channel, response);
logger.info("Client {} response sent", clientId);
// Verifica se é comando CLOSE
String connectionHeader = response.getHeadersOrDefault("Connection", "");
if ("close".equals(connectionHeader)) {
logger.info("Client {} requested connection close", clientId);
break; // Sai do loop e fecha
}
}
} catch (IOException e) {
if (channel.isOpen()) {
logger.error("IO error handling client {}", clientId, e);
} else {
logger.debug("Client {} connection closed by peer", clientId);
}
} catch (Exception e) {
logger.error("Unexpected error handling client {}", clientId, e);
} finally {
try {
if (channel.isOpen()) {
channel.close();
}
logger.debug("Client {} channel closed", clientId);
} catch (IOException e) {
logger.error("Error closing client {} channel", clientId, e);
}
}
}
private TransportProtocol.Request readRequest(SocketChannel channel) throws IOException {
// Primeiro, lê o tamanho da mensagem (4 bytes = int32)
java.nio.ByteBuffer sizeBuffer = java.nio.ByteBuffer.allocate(4);
int bytesRead = 0;
while (bytesRead < 4) {
int read = channel.read(sizeBuffer);
if (read == -1) {
logger.debug("Client disconnected before sending size");
return null;
}
bytesRead += read;
}
sizeBuffer.flip();
int messageSize = sizeBuffer.getInt();
logger.debug("Expecting message of {} bytes", messageSize);
// Validação básica de segurança
if (messageSize <= 0 || messageSize > 1024 * 1024) { // Max 1MB
throw new IOException("Invalid message size: " + messageSize);
}
// Agora lê a mensagem completa
java.nio.ByteBuffer messageBuffer = java.nio.ByteBuffer.allocate(messageSize);
bytesRead = 0;
while (bytesRead < messageSize) {
int read = channel.read(messageBuffer);
if (read == -1) {
throw new IOException("Client disconnected while reading message");
}
bytesRead += read;
}
messageBuffer.flip();
// Deserializa o Protocol Buffers
byte[] messageBytes = new byte[messageSize];
messageBuffer.get(messageBytes);
return TransportProtocol.Request.parseFrom(messageBytes);
}
private void writeResponse(SocketChannel channel, TransportProtocol.Response response) throws IOException {
byte[] messageBytes = response.toByteArray();
int messageSize = messageBytes.length;
logger.debug("Writing response of {} bytes", messageSize);
java.nio.ByteBuffer buffer = java.nio.ByteBuffer.allocate(4 + messageSize);
buffer.putInt(messageSize);
buffer.put(messageBytes);
buffer.flip();
while (buffer.hasRemaining()) {
channel.write(buffer);
}
logger.debug("Response written successfully");
}
}
}
+53
View File
@@ -0,0 +1,53 @@
syntax = "proto3";
option java_package = "com.mediamanager.protocol.messages";
option java_outer_classname = "GenreMessages";
package mediamanager.messages;
message Genre {
int32 id = 1;
string name = 2;
}
message CreateGenreRequest {
string name = 1;
}
message CreateGenreResponse {
Genre genre = 1;
}
message GetGenresRequest {
}
message GetGenresResponse {
repeated Genre genres = 1;
}
message GetGenreByIdRequest {
int32 id = 1;
}
message GetGenreByIdResponse {
Genre genre = 1;
}
message UpdateGenreRequest {
int32 id = 1;
string name = 2; // Novo nome
}
message UpdateGenreResponse {
Genre genre = 1; // Genre atualizado
}
message DeleteGenreRequest {
int32 id = 1;
}
message DeleteGenreResponse {
bool success = 1;
string message = 2;
}
+19
View File
@@ -0,0 +1,19 @@
syntax = "proto3";
option java_package = "com.mediamanager.protocol";
option java_outer_classname = "TransportProtocol";
package mediamanager;
message Request {
string request_id = 1;
bytes payload = 2;
map<string, string> headers = 3;
}
message Response {
string request_id = 1;
int32 status_code = 2;
bytes payload = 3;
map<string, string> headers = 4;
}
+30
View File
@@ -0,0 +1,30 @@
syntax = "proto3";
option java_package = "com.mediamanager.protocol";
option java_outer_classname = "TestProtocol";
package mediamanager.test;
message EchoCommand {
string message = 1;
}
message EchoResponse {
string message = 1;
int64 server_timestamp = 2;
}
message HeartbeatCommand {
int64 client_timestamp = 1;
}
message HeartbeatResponse {
int64 client_timestamp = 1;
int64 server_timestamp = 2;
}
message CloseCommand {
// Vazio - apenas sinaliza fechamento
}
message CloseResponse {
string message = 1;
}
@@ -21,3 +21,8 @@ hibernate.format_sql=true
ipc.pipe.name=mediamanager-pipe
ipc.pipe.path=/tmp/mediamanager
ipc.buffer.size=8192
runtype=local
ipc.socket.path=/tmp/mediamanager
+7 -6
View File
@@ -1,12 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Configuration status="WARN" shutdownHook="disable">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<Console name="Console" target="SYSTEM_OUT" immediateFlush="true">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
<RollingFile name="FileAppender" fileName="logs/mediamanager.log"
filePattern="logs/mediamanager-%d{yyyy-MM-dd}-%i.log">
filePattern="logs/mediamanager-%d{yyyy-MM-dd}-%i.log"
immediateFlush="true">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
@@ -17,17 +18,17 @@
</Appenders>
<Loggers>
<Logger name="com.mediamanager" level="debug" additivity="false">
<Logger name="com.mediamanager" level="INFO" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="FileAppender"/>
</Logger>
<Logger name="org.hibernate" level="info" additivity="false">
<Logger name="org.hibernate" level="INFO" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="FileAppender"/>
</Logger>
<Root level="info">
<Root level="INFO">
<AppenderRef ref="Console"/>
<AppenderRef ref="FileAppender"/>
</Root>