Versions stable (inachevée mais prête à un déploiement en prod)
This commit is contained in:
BIN
.gitignore
vendored
BIN
.gitignore
vendored
Binary file not shown.
415
pom.xml
415
pom.xml
@@ -1,111 +1,322 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<groupId>dev.lions</groupId>
|
<groupId>dev.lions</groupId>
|
||||||
<artifactId>lionsdev-client-impl-quarkus</artifactId>
|
<artifactId>lionsdev-client-impl-quarkus</artifactId>
|
||||||
<version>1.0.0-SNAPSHOT</version>
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
<name>Lions Dev - Application Client Quarkus</name>
|
||||||
|
<description>Application client implémentant les services de Lions Dev avec Quarkus</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<compiler-plugin.version>3.13.0</compiler-plugin.version>
|
<!-- Versions -->
|
||||||
<maven.compiler.release>17</maven.compiler.release>
|
<compiler-plugin.version>3.13.0</compiler-plugin.version>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<myfaces.version>4.0.1</myfaces.version>
|
||||||
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
<primefaces.version>13.0.5</primefaces.version>
|
||||||
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
|
<quarkus.platform.version>3.7.3</quarkus.platform.version>
|
||||||
<quarkus.platform.version>3.6.4</quarkus.platform.version>
|
<lombok.version>1.18.32</lombok.version>
|
||||||
<skipITs>true</skipITs>
|
<jackson.version>2.17.0</jackson.version>
|
||||||
<surefire-plugin.version>3.5.0</surefire-plugin.version>
|
<elasticsearch.version>8.12.2</elasticsearch.version>
|
||||||
<myfaces.version>4.0.1</myfaces.version>
|
<guava.version>33.0.0-jre</guava.version>
|
||||||
<primefaces.version>13.0.4</primefaces.version>
|
<jakarta.mail.version>2.1.3</jakarta.mail.version>
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependencyManagement>
|
<!-- Configuration du projet -->
|
||||||
<dependencies>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<dependency>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
<groupId>${quarkus.platform.group-id}</groupId>
|
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
||||||
<artifactId>${quarkus.platform.artifact-id}</artifactId>
|
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
|
||||||
<version>${quarkus.platform.version}</version>
|
|
||||||
<type>pom</type>
|
|
||||||
<scope>import</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</dependencyManagement>
|
|
||||||
|
|
||||||
|
<!-- Tests -->
|
||||||
|
<skipITs>true</skipITs>
|
||||||
|
<surefire-plugin.version>3.2.5</surefire-plugin.version>
|
||||||
|
<jacoco.version>0.8.11</jacoco.version>
|
||||||
|
<sonar.version>3.10.0.2594</sonar.version>
|
||||||
|
|
||||||
|
<!-- Qualité du code -->
|
||||||
|
<sonar.host.url>http://localhost:9000</sonar.host.url>
|
||||||
|
<sonar.java.source>17</sonar.java.source>
|
||||||
|
<sonar.coverage.exclusions>
|
||||||
|
**/models/**,
|
||||||
|
**/exceptions/**
|
||||||
|
</sonar.coverage.exclusions>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- Quarkus Core -->
|
<dependency>
|
||||||
<dependency>
|
<groupId>${quarkus.platform.group-id}</groupId>
|
||||||
<groupId>io.quarkus</groupId>
|
<artifactId>${quarkus.platform.artifact-id}</artifactId>
|
||||||
<artifactId>quarkus-undertow</artifactId>
|
<version>${quarkus.platform.version}</version>
|
||||||
</dependency>
|
<type>pom</type>
|
||||||
<dependency>
|
<scope>import</scope>
|
||||||
<groupId>io.quarkus</groupId>
|
</dependency>
|
||||||
<artifactId>quarkus-arc</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- MyFaces -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.myfaces.core</groupId>
|
|
||||||
<artifactId>myfaces-api</artifactId>
|
|
||||||
<version>${myfaces.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.myfaces.core</groupId>
|
|
||||||
<artifactId>myfaces-impl</artifactId>
|
|
||||||
<version>${myfaces.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- PrimeFaces -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.quarkiverse.primefaces</groupId>
|
|
||||||
<artifactId>quarkus-primefaces</artifactId>
|
|
||||||
<version>3.14.0</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Test -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.quarkus</groupId>
|
|
||||||
<artifactId>quarkus-junit5</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
<build>
|
<dependencies>
|
||||||
<plugins>
|
<!-- Quarkus Core -->
|
||||||
<plugin>
|
<dependency>
|
||||||
<groupId>${quarkus.platform.group-id}</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-maven-plugin</artifactId>
|
<artifactId>quarkus-core</artifactId>
|
||||||
<version>${quarkus.platform.version}</version>
|
</dependency>
|
||||||
<extensions>true</extensions>
|
<dependency>
|
||||||
<executions>
|
<groupId>io.quarkus</groupId>
|
||||||
<execution>
|
<artifactId>quarkus-arc</artifactId>
|
||||||
<goals>
|
</dependency>
|
||||||
<goal>build</goal>
|
|
||||||
<goal>generate-code</goal>
|
<!-- Web & UI -->
|
||||||
<goal>generate-code-tests</goal>
|
<dependency>
|
||||||
</goals>
|
<groupId>io.quarkus</groupId>
|
||||||
</execution>
|
<artifactId>quarkus-undertow</artifactId>
|
||||||
</executions>
|
</dependency>
|
||||||
</plugin>
|
<dependency>
|
||||||
<plugin>
|
<groupId>org.apache.myfaces.core</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>myfaces-api</artifactId>
|
||||||
<version>${compiler-plugin.version}</version>
|
<version>${myfaces.version}</version>
|
||||||
<configuration>
|
</dependency>
|
||||||
<parameters>true</parameters>
|
<dependency>
|
||||||
</configuration>
|
<groupId>org.apache.myfaces.core</groupId>
|
||||||
</plugin>
|
<artifactId>myfaces-impl</artifactId>
|
||||||
<plugin>
|
<version>${myfaces.version}</version>
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
</dependency>
|
||||||
<version>${surefire-plugin.version}</version>
|
<dependency>
|
||||||
<configuration>
|
<groupId>io.quarkiverse.primefaces</groupId>
|
||||||
<systemPropertyVariables>
|
<artifactId>quarkus-primefaces</artifactId>
|
||||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
<version>3.14.0</version>
|
||||||
<maven.home>${maven.home}</maven.home>
|
</dependency>
|
||||||
</systemPropertyVariables>
|
<dependency>
|
||||||
</configuration>
|
<groupId>org.apache.myfaces.core.extensions.quarkus</groupId>
|
||||||
</plugin>
|
<artifactId>myfaces-quarkus</artifactId>
|
||||||
</plugins>
|
<version>4.0.1</version>
|
||||||
</build>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Persistence -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-hibernate-orm</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-hibernate-validator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-jdbc-postgresql</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Cache & Performance -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-cache</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
<version>${guava.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Messaging & Communication -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-mailer</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-websockets</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.mail</groupId>
|
||||||
|
<artifactId>jakarta.mail-api</artifactId>
|
||||||
|
<version>${jakarta.mail.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Elasticsearch -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-elasticsearch-rest-client</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>co.elastic.clients</groupId>
|
||||||
|
<artifactId>elasticsearch-java</artifactId>
|
||||||
|
<version>${elasticsearch.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Utilitaires -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Monitoring & API -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-smallrye-health</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-smallrye-openapi</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-swagger-ui</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-micrometer</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Tests -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-junit5</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.rest-assured</groupId>
|
||||||
|
<artifactId>rest-assured</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-vertx-http</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-resteasy-reactive</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Sécurité -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-elytron-security-properties-file</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<!-- Plugin Quarkus -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>${quarkus.platform.group-id}</groupId>
|
||||||
|
<artifactId>quarkus-maven-plugin</artifactId>
|
||||||
|
<version>${quarkus.platform.version}</version>
|
||||||
|
<extensions>true</extensions>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>build</goal>
|
||||||
|
<goal>generate-code</goal>
|
||||||
|
<goal>generate-code-tests</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Compilation Java -->
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>${compiler-plugin.version}</version>
|
||||||
|
<configuration>
|
||||||
|
<parameters>true</parameters>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Tests unitaires -->
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>${surefire-plugin.version}</version>
|
||||||
|
<configuration>
|
||||||
|
<systemPropertyVariables>
|
||||||
|
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||||
|
<maven.home>${maven.home}</maven.home>
|
||||||
|
</systemPropertyVariables>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Couverture de code -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jacoco</groupId>
|
||||||
|
<artifactId>jacoco-maven-plugin</artifactId>
|
||||||
|
<version>${jacoco.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>prepare-agent</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>report</id>
|
||||||
|
<phase>test</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>report</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Analyse qualité -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.sonarsource.scanner.maven</groupId>
|
||||||
|
<artifactId>sonar-maven-plugin</artifactId>
|
||||||
|
<version>${sonar.version}</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>native</id>
|
||||||
|
<activation>
|
||||||
|
<property>
|
||||||
|
<name>native</name>
|
||||||
|
</property>
|
||||||
|
</activation>
|
||||||
|
<properties>
|
||||||
|
<skipITs>false</skipITs>
|
||||||
|
<quarkus.package.type>native</quarkus.package.type>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
<profile>
|
||||||
|
<id>development</id>
|
||||||
|
<properties>
|
||||||
|
<quarkus.package.type>jar</quarkus.package.type>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
<profile>
|
||||||
|
<id>production</id>
|
||||||
|
<properties>
|
||||||
|
<quarkus.package.type>jar</quarkus.package.type>
|
||||||
|
<quarkus.package.uber-jar>true</quarkus.package.uber-jar>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
</project>
|
</project>
|
||||||
@@ -93,5 +93,10 @@ USER 185
|
|||||||
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
|
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
|
||||||
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
|
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
|
||||||
|
|
||||||
|
# Ajout des options pour le monitoring JVM
|
||||||
|
ENV JAVA_OPTS=""
|
||||||
|
ENV JAVA_OPTS="${JAVA_OPTS} -XX:+UseParallelGC -XX:MaxRAMPercentage=75.0 -XX:+HeapDumpOnOutOfMemoryError"
|
||||||
|
|
||||||
|
|
||||||
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
|
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
|
||||||
|
|
||||||
|
|||||||
BIN
src/main/docker/backups/daily/lionsdev_db-20241215.sql.gz
Normal file
BIN
src/main/docker/backups/daily/lionsdev_db-20241215.sql.gz
Normal file
Binary file not shown.
1
src/main/docker/backups/daily/lionsdev_db-latest.sql.gz
Symbolic link
1
src/main/docker/backups/daily/lionsdev_db-latest.sql.gz
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
lionsdev_db-20241215.sql.gz
|
||||||
BIN
src/main/docker/backups/last/lionsdev_db-20241215-121555.sql.gz
Normal file
BIN
src/main/docker/backups/last/lionsdev_db-20241215-121555.sql.gz
Normal file
Binary file not shown.
1
src/main/docker/backups/last/lionsdev_db-latest.sql.gz
Symbolic link
1
src/main/docker/backups/last/lionsdev_db-latest.sql.gz
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
lionsdev_db-20241215-121555.sql.gz
|
||||||
BIN
src/main/docker/backups/monthly/lionsdev_db-202412.sql.gz
Normal file
BIN
src/main/docker/backups/monthly/lionsdev_db-202412.sql.gz
Normal file
Binary file not shown.
1
src/main/docker/backups/monthly/lionsdev_db-latest.sql.gz
Symbolic link
1
src/main/docker/backups/monthly/lionsdev_db-latest.sql.gz
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
lionsdev_db-202412.sql.gz
|
||||||
BIN
src/main/docker/backups/weekly/lionsdev_db-202450.sql.gz
Normal file
BIN
src/main/docker/backups/weekly/lionsdev_db-202450.sql.gz
Normal file
Binary file not shown.
1
src/main/docker/backups/weekly/lionsdev_db-latest.sql.gz
Symbolic link
1
src/main/docker/backups/weekly/lionsdev_db-latest.sql.gz
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
lionsdev_db-202450.sql.gz
|
||||||
173
src/main/docker/docker-compose.yml
Normal file
173
src/main/docker/docker-compose.yml
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Déclaration des services pour votre application Quarkus avec PostgreSQL, pgAdmin, Prometheus, Grafana et les exporters.
|
||||||
|
|
||||||
|
services:
|
||||||
|
#-----------------------------------------------------------------------------
|
||||||
|
# Service principal : Application Quarkus
|
||||||
|
#-----------------------------------------------------------------------------
|
||||||
|
quarkus-app:
|
||||||
|
container_name: ${APP_NAME}-app
|
||||||
|
image: dahoudg/lionsdev-client-impl-quarkus-jvm:latest
|
||||||
|
build:
|
||||||
|
context: ./
|
||||||
|
dockerfile: Dockerfile.jvm
|
||||||
|
args:
|
||||||
|
- JAVA_VERSION=${JAVA_VERSION}
|
||||||
|
environment:
|
||||||
|
# Configuration de la base de données
|
||||||
|
- QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||||
|
- QUARKUS_DATASOURCE_USERNAME=${POSTGRES_USER}
|
||||||
|
- QUARKUS_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
|
# Configuration du serveur
|
||||||
|
- QUARKUS_HTTP_PORT=${QUARKUS_HTTP_PORT}
|
||||||
|
- TZ=${TZ}
|
||||||
|
# Configuration des chemins et stockage
|
||||||
|
- APP_STORAGE_BASE_PATH=/app/storage
|
||||||
|
- APP_BASE_URL=${APP_BASE_URL:-http://localhost:8080}
|
||||||
|
- APP_ENVIRONMENT=${ENVIRONMENT:-production}
|
||||||
|
# Configuration des logs
|
||||||
|
- QUARKUS_LOG_FILE_ENABLE=true
|
||||||
|
- QUARKUS_LOG_FILE_PATH=/var/log/lionsdev/application.log
|
||||||
|
- QUARKUS_LOG_LEVEL=INFO
|
||||||
|
volumes:
|
||||||
|
- ./logs:/var/log/lionsdev
|
||||||
|
- ./storage:/app/storage
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:${QUARKUS_HTTP_PORT}/q/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
ports:
|
||||||
|
- "${QUARKUS_HTTP_PORT}:8080" # Expose uniquement le port nécessaire pour Quarkus
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '${QUARKUS_CPU_LIMIT}'
|
||||||
|
memory: ${QUARKUS_MEMORY_LIMIT}
|
||||||
|
depends_on:
|
||||||
|
postgres-db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
#-----------------------------------------------------------------------------
|
||||||
|
# Base de données : PostgreSQL
|
||||||
|
#-----------------------------------------------------------------------------
|
||||||
|
postgres-db:
|
||||||
|
container_name: ${APP_NAME}-db
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
TZ: ${TZ}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1.0'
|
||||||
|
memory: 1G
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
#-----------------------------------------------------------------------------
|
||||||
|
# Interface d'administration PostgreSQL : pgAdmin
|
||||||
|
#-----------------------------------------------------------------------------
|
||||||
|
pgadmin:
|
||||||
|
container_name: ${APP_NAME}-pgadmin
|
||||||
|
image: dpage/pgadmin4:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL}
|
||||||
|
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD}
|
||||||
|
TZ: ${TZ}
|
||||||
|
ports:
|
||||||
|
- "${PGADMIN_PORT}:80"
|
||||||
|
volumes:
|
||||||
|
- pgadmin-data:/var/lib/pgadmin
|
||||||
|
depends_on:
|
||||||
|
postgres-db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
#-----------------------------------------------------------------------------
|
||||||
|
# Monitoring : Prometheus
|
||||||
|
#-----------------------------------------------------------------------------
|
||||||
|
prometheus:
|
||||||
|
container_name: ${APP_NAME}-prometheus
|
||||||
|
image: prom/prometheus:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||||
|
- prometheus-data:/prometheus
|
||||||
|
ports:
|
||||||
|
- "${PROMETHEUS_PORT}:9090"
|
||||||
|
depends_on:
|
||||||
|
- postgres-exporter
|
||||||
|
- node-exporter
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
#-----------------------------------------------------------------------------
|
||||||
|
# Exporters pour PostgreSQL et le serveur
|
||||||
|
#-----------------------------------------------------------------------------
|
||||||
|
postgres-exporter:
|
||||||
|
container_name: ${APP_NAME}-postgres-exporter
|
||||||
|
image: prometheuscommunity/postgres-exporter:latest
|
||||||
|
environment:
|
||||||
|
DATA_SOURCE_NAME: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres-db:5432/${POSTGRES_DB}?sslmode=disable"
|
||||||
|
ports:
|
||||||
|
- "${POSTGRES_EXPORTER_PORT}:9187"
|
||||||
|
depends_on:
|
||||||
|
postgres-db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
node-exporter:
|
||||||
|
container_name: ${APP_NAME}-node-exporter
|
||||||
|
image: prom/node-exporter:latest
|
||||||
|
ports:
|
||||||
|
- "${NODE_EXPORTER_PORT}:9100"
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
#-----------------------------------------------------------------------------
|
||||||
|
# Visualisation : Grafana
|
||||||
|
#-----------------------------------------------------------------------------
|
||||||
|
grafana:
|
||||||
|
container_name: ${APP_NAME}-grafana
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER}
|
||||||
|
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD}
|
||||||
|
TZ: ${TZ}
|
||||||
|
ports:
|
||||||
|
- "${GRAFANA_PORT}:3000"
|
||||||
|
volumes:
|
||||||
|
- grafana-data:/var/lib/grafana
|
||||||
|
depends_on:
|
||||||
|
- prometheus
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
pgadmin-data:
|
||||||
|
prometheus-data:
|
||||||
|
grafana-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
11
src/main/docker/init.sql
Normal file
11
src/main/docker/init.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- Création de la base de données si elle n'existe pas
|
||||||
|
CREATE DATABASE IF NOT EXISTS lionsdev_db;
|
||||||
|
|
||||||
|
-- Configuration des droits d'accès
|
||||||
|
ALTER DATABASE lionsdev_db OWNER TO lions_admin_db;
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE lionsdev_db TO lions_admin_db;
|
||||||
|
|
||||||
|
-- Configuration des schémas nécessaires
|
||||||
|
\c lionsdev_db
|
||||||
|
CREATE SCHEMA IF NOT EXISTS public;
|
||||||
|
GRANT ALL ON SCHEMA public TO lions_admin_db;
|
||||||
29
src/main/docker/nginx/nginx.conf
Normal file
29
src/main/docker/nginx/nginx.conf
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
events {}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name lions.dev www.lions.dev;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://quarkus-app:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name pgadmin.lions.dev;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://pgadmin:80;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/main/docker/prometheus/prometheus.yml
Normal file
61
src/main/docker/prometheus/prometheus.yml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Configuration Prometheus corrigée
|
||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
evaluation_interval: 15s
|
||||||
|
external_labels:
|
||||||
|
monitor: 'lions-portal-monitor'
|
||||||
|
|
||||||
|
rule_files:
|
||||||
|
- "rules/*rules.yml"
|
||||||
|
- "alerts/*alerts.yml"
|
||||||
|
|
||||||
|
alerting:
|
||||||
|
alertmanagers:
|
||||||
|
- static_configs:
|
||||||
|
- targets:
|
||||||
|
- 'alertmanager:9093'
|
||||||
|
scrape_configs:
|
||||||
|
# Application Quarkus metrics
|
||||||
|
- job_name: 'quarkus'
|
||||||
|
metrics_path: '/q/metrics'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['quarkus-app:8080']
|
||||||
|
scrape_interval: 10s
|
||||||
|
|
||||||
|
# Postgres Exporter metrics
|
||||||
|
- job_name: 'postgres'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['postgres-exporter:9187']
|
||||||
|
|
||||||
|
# Nginx metrics
|
||||||
|
- job_name: 'nginx'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['nginx-exporter:9113']
|
||||||
|
|
||||||
|
# Node Exporter metrics (system metrics)
|
||||||
|
- job_name: 'node'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['node-exporter:9100']
|
||||||
|
|
||||||
|
# Prometheus self-monitoring
|
||||||
|
- job_name: 'prometheus'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['localhost:9090']
|
||||||
|
|
||||||
|
# Nginx Status Page
|
||||||
|
- job_name: 'nginx-status'
|
||||||
|
metrics_path: /stub_status
|
||||||
|
static_configs:
|
||||||
|
- targets: ['nginx:80']
|
||||||
|
|
||||||
|
# Grafana metrics
|
||||||
|
- job_name: 'grafana'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['grafana:3000']
|
||||||
|
|
||||||
|
# Node Exporter Host metrics
|
||||||
|
- job_name: 'node_exporter_host'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['node-exporter:9100']
|
||||||
|
labels:
|
||||||
|
instance: 'host'
|
||||||
1
src/main/docker/secrets/db_password.txt
Normal file
1
src/main/docker/secrets/db_password.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
kJ9#mP2@nQ8&xR3
|
||||||
1
src/main/docker/secrets/db_user.txt
Normal file
1
src/main/docker/secrets/db_user.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
lions_admin_db
|
||||||
1
src/main/docker/secrets/grafana_admin_password.txt
Normal file
1
src/main/docker/secrets/grafana_admin_password.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
tR6#fL9@wQ7&nX2%pY5
|
||||||
1
src/main/docker/secrets/grafana_admin_user.txt
Normal file
1
src/main/docker/secrets/grafana_admin_user.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
lions_admin
|
||||||
1
src/main/docker/secrets/pgadmin_email.txt
Normal file
1
src/main/docker/secrets/pgadmin_email.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
admin@lions.dev
|
||||||
1
src/main/docker/secrets/pgadmin_password.txt
Normal file
1
src/main/docker/secrets/pgadmin_password.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
hN7%pW4#cM9@jX5&vY8
|
||||||
171
src/main/java/dev/lions/components/ChartComponent.java
Normal file
171
src/main/java/dev/lions/components/ChartComponent.java
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
package dev.lions.components;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.faces.view.ViewScoped;
|
||||||
|
import jakarta.inject.Named;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.primefaces.model.charts.*;
|
||||||
|
import org.primefaces.model.charts.bar.*;
|
||||||
|
import org.primefaces.model.charts.line.*;
|
||||||
|
import org.primefaces.model.charts.pie.*;
|
||||||
|
import org.primefaces.model.charts.optionconfig.title.Title;
|
||||||
|
import org.primefaces.model.charts.optionconfig.legend.Legend;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composant de gestion des graphiques.
|
||||||
|
* Fournit des modèles pour les graphiques linéaires, en barres et circulaires.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 2.1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Named
|
||||||
|
@ViewScoped
|
||||||
|
public class ChartComponent implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private static final List<String> CHART_COLORS = Arrays.asList(
|
||||||
|
"rgba(33, 150, 243, 0.8)",
|
||||||
|
"rgba(255, 64, 129, 0.8)",
|
||||||
|
"rgba(255, 193, 7, 0.8)",
|
||||||
|
"rgba(76, 175, 80, 0.8)",
|
||||||
|
"rgba(156, 39, 176, 0.8)"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
private LineChartModel lineModel;
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
private BarChartModel barModel;
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
private PieChartModel pieModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les modèles de graphiques lors de la construction du composant.
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
log.info("Initialisation des modèles de graphiques.");
|
||||||
|
createLineModel();
|
||||||
|
createBarModel();
|
||||||
|
createPieModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un modèle de graphique linéaire.
|
||||||
|
*/
|
||||||
|
private void createLineModel() {
|
||||||
|
lineModel = new LineChartModel();
|
||||||
|
ChartData data = new ChartData();
|
||||||
|
|
||||||
|
LineChartDataSet dataSet = new LineChartDataSet();
|
||||||
|
dataSet.setLabel("Évolution des ventes");
|
||||||
|
dataSet.setData(new ArrayList<>(generateRandomData(6)));
|
||||||
|
dataSet.setBorderColor(CHART_COLORS.get(0));
|
||||||
|
dataSet.setFill(false);
|
||||||
|
dataSet.setTension(0.4);
|
||||||
|
|
||||||
|
data.addChartDataSet(dataSet);
|
||||||
|
data.setLabels(generateLabels(6, "Mois"));
|
||||||
|
|
||||||
|
lineModel.setData(data);
|
||||||
|
addChartOptions(lineModel, "Évolution temporelle");
|
||||||
|
|
||||||
|
log.debug("Modèle de graphique linéaire créé.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un modèle de graphique en barres.
|
||||||
|
*/
|
||||||
|
private void createBarModel() {
|
||||||
|
barModel = new BarChartModel();
|
||||||
|
ChartData data = new ChartData();
|
||||||
|
|
||||||
|
BarChartDataSet dataSet = new BarChartDataSet();
|
||||||
|
dataSet.setLabel("Performance par trimestre");
|
||||||
|
dataSet.setData(new ArrayList<>(generateRandomData(4)));
|
||||||
|
dataSet.setBackgroundColor(CHART_COLORS.get(1));
|
||||||
|
|
||||||
|
data.addChartDataSet(dataSet);
|
||||||
|
data.setLabels(generateLabels(4, "T"));
|
||||||
|
|
||||||
|
barModel.setData(data);
|
||||||
|
addChartOptions(barModel, "Performance trimestrielle");
|
||||||
|
|
||||||
|
log.debug("Modèle de graphique en barres créé.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un modèle de graphique circulaire.
|
||||||
|
*/
|
||||||
|
private void createPieModel() {
|
||||||
|
pieModel = new PieChartModel();
|
||||||
|
ChartData data = new ChartData();
|
||||||
|
|
||||||
|
PieChartDataSet dataSet = new PieChartDataSet();
|
||||||
|
dataSet.setData(Arrays.asList(25, 35, 40));
|
||||||
|
dataSet.setBackgroundColor(CHART_COLORS);
|
||||||
|
|
||||||
|
data.addChartDataSet(dataSet);
|
||||||
|
data.setLabels(Arrays.asList("Développement", "Marketing", "Infrastructure"));
|
||||||
|
|
||||||
|
pieModel.setData(data);
|
||||||
|
addChartOptions(pieModel, "Répartition des activités");
|
||||||
|
|
||||||
|
log.debug("Modèle de graphique circulaire créé.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute des options telles que le titre et la légende aux graphiques.
|
||||||
|
*
|
||||||
|
* @param model Le modèle de graphique.
|
||||||
|
* @param title Titre du graphique.
|
||||||
|
*/
|
||||||
|
private void addChartOptions(ChartModel model, String title) {
|
||||||
|
Title chartTitle = new Title();
|
||||||
|
chartTitle.setDisplay(true);
|
||||||
|
chartTitle.setText(title);
|
||||||
|
|
||||||
|
Legend legend = new Legend();
|
||||||
|
legend.setDisplay(true);
|
||||||
|
legend.setPosition("bottom");
|
||||||
|
|
||||||
|
if (model instanceof LineChartModel) {
|
||||||
|
LineChartModel lineChart = (LineChartModel) model;
|
||||||
|
lineChart.setExtender((String) chartTitle.getText());
|
||||||
|
} else if (model instanceof BarChartModel) {
|
||||||
|
BarChartModel barChart = (BarChartModel) model;
|
||||||
|
barChart.setExtender((String) chartTitle.getText());
|
||||||
|
} else if (model instanceof PieChartModel) {
|
||||||
|
PieChartModel pieChart = (PieChartModel) model;
|
||||||
|
pieChart.setExtender((String) chartTitle.getText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Number> generateRandomData(int size) {
|
||||||
|
Random random = new Random();
|
||||||
|
List<Number> data = new ArrayList<>();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
data.add(random.nextInt(100));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> generateLabels(int size, String prefix) {
|
||||||
|
List<String> labels = new ArrayList<>();
|
||||||
|
for (int i = 1; i <= size; i++) {
|
||||||
|
labels.add(prefix + " " + i);
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/main/java/dev/lions/components/DataTableView.java
Normal file
4
src/main/java/dev/lions/components/DataTableView.java
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package dev.lions.components;
|
||||||
|
|
||||||
|
public class DataTableView {
|
||||||
|
}
|
||||||
295
src/main/java/dev/lions/components/DynamicDataTable.java
Normal file
295
src/main/java/dev/lions/components/DynamicDataTable.java
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
package dev.lions.components;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.Dependent;
|
||||||
|
import jakarta.faces.view.ViewScoped;
|
||||||
|
import jakarta.inject.Named;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.primefaces.model.FilterMeta;
|
||||||
|
import org.primefaces.model.LazyDataModel;
|
||||||
|
import org.primefaces.model.SortMeta;
|
||||||
|
import org.primefaces.event.data.PageEvent;
|
||||||
|
|
||||||
|
import dev.lions.utils.Column;
|
||||||
|
import dev.lions.utils.FilterCriteria;
|
||||||
|
import dev.lions.exceptions.DataTableException;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composant de tableau de données dynamique avec support de pagination, tri et filtrage.
|
||||||
|
* Fournit une interface riche et performante pour l'affichage et la manipulation des données
|
||||||
|
* tabulaires dans l'application.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 2.1
|
||||||
|
*/
|
||||||
|
@Named
|
||||||
|
@Dependent
|
||||||
|
@Slf4j
|
||||||
|
public class DynamicDataTable<T> implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private static final int DEFAULT_PAGE_SIZE = 10;
|
||||||
|
private static final int MAX_PAGE_SIZE = 100;
|
||||||
|
private static final String DEFAULT_EMPTY_MESSAGE = "Aucune donnée disponible";
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
private List<T> data;
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
private List<Column> columns;
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
private String emptyMessage = DEFAULT_EMPTY_MESSAGE;
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
@Min(1)
|
||||||
|
private int pageSize = DEFAULT_PAGE_SIZE;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private LazyDataModel<T> lazyModel;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Map<String, FilterCriteria> activeFilters = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final Map<String, Comparator<T>> customSorters = new HashMap<>();
|
||||||
|
private final Map<String, PropertyAccessor<T>> propertyAccessors = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le tableau avec les données et les colonnes spécifiées.
|
||||||
|
*
|
||||||
|
* @param data Données à afficher
|
||||||
|
* @param columns Configuration des colonnes
|
||||||
|
*/
|
||||||
|
public void initialize(@NotNull List<T> data, @NotNull List<Column> columns) {
|
||||||
|
log.info("Initialisation du tableau dynamique avec {} enregistrements", data.size());
|
||||||
|
|
||||||
|
validateInitializationParameters(data, columns);
|
||||||
|
|
||||||
|
this.data = new ArrayList<>(data);
|
||||||
|
this.columns = new ArrayList<>(columns);
|
||||||
|
|
||||||
|
initializePropertyAccessors();
|
||||||
|
initializeLazyLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure le modèle de chargement paresseux des données.
|
||||||
|
*/
|
||||||
|
private void initializeLazyLoading() {
|
||||||
|
lazyModel = new LazyDataModel<T>() {
|
||||||
|
@Override
|
||||||
|
public List<T> load(int first, int pageSize, Map<String, SortMeta> sortBy, Map<String, FilterMeta> filterBy) {
|
||||||
|
try {
|
||||||
|
return loadDataPage(first, pageSize, sortBy, filterBy);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors du chargement des données", e);
|
||||||
|
throw new DataTableException("Échec du chargement des données", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int count(Map<String, FilterMeta> filterBy) {
|
||||||
|
return data == null ? 0 : data.size();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
lazyModel.setRowCount(data.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge une page de données selon les critères spécifiés.
|
||||||
|
*/
|
||||||
|
protected List<T> loadDataPage(int first, int pageSize, Map<String, SortMeta> sortBy, Map<String, FilterMeta> filterBy) {
|
||||||
|
return data.stream()
|
||||||
|
.filter(item -> applyFilters(item, filterBy))
|
||||||
|
.sorted((a, b) -> applySorting(a, b, sortBy))
|
||||||
|
.skip(first)
|
||||||
|
.limit(pageSize)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applique les filtres sur un élément.
|
||||||
|
*/
|
||||||
|
private boolean applyFilters(T item, Map<String, FilterMeta> filterBy) {
|
||||||
|
if (filterBy == null || filterBy.isEmpty()) return true;
|
||||||
|
|
||||||
|
return filterBy.entrySet().stream().allMatch(entry -> {
|
||||||
|
Object filterValue = entry.getValue().getFilterValue();
|
||||||
|
if (filterValue == null) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object value = getPropertyValue(item, entry.getKey());
|
||||||
|
return value != null && value.toString().toLowerCase().contains(filterValue.toString().toLowerCase());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Erreur lors du filtrage", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un élément correspond à un critère de filtrage.
|
||||||
|
*/
|
||||||
|
private boolean matchesFilter(T item, String property, Object filterValue) {
|
||||||
|
if (filterValue == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object value = getPropertyValue(item, property);
|
||||||
|
return compareValues(value, filterValue);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Erreur lors du filtrage de la propriété: {}", property, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare deux valeurs pour le filtrage.
|
||||||
|
*/
|
||||||
|
private boolean compareValues(Object value, Object filterValue) {
|
||||||
|
if (value == null) {
|
||||||
|
return filterValue == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String valueStr = value.toString().toLowerCase();
|
||||||
|
String filterStr = filterValue.toString().toLowerCase();
|
||||||
|
return valueStr.contains(filterStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applique le tri sur les données.
|
||||||
|
*/
|
||||||
|
private int applySorting(T a, T b, Map<String, SortMeta> sortBy) {
|
||||||
|
for (Map.Entry<String, SortMeta> entry : sortBy.entrySet()) {
|
||||||
|
String property = entry.getKey();
|
||||||
|
try {
|
||||||
|
Comparable valueA = (Comparable) getPropertyValue(a, property);
|
||||||
|
Comparable valueB = (Comparable) getPropertyValue(b, property);
|
||||||
|
|
||||||
|
int result = valueA.compareTo(valueB);
|
||||||
|
return entry.getValue().getOrder().isAscending() ? result : -result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Erreur lors du tri", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare les valeurs de deux propriétés pour le tri.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private int compareProperties(T a, T b, String property) {
|
||||||
|
try {
|
||||||
|
Comparable valueA = (Comparable) getPropertyValue(a, property);
|
||||||
|
Comparable valueB = (Comparable) getPropertyValue(b, property);
|
||||||
|
|
||||||
|
if (valueA == null && valueB == null) return 0;
|
||||||
|
if (valueA == null) return -1;
|
||||||
|
if (valueB == null) return 1;
|
||||||
|
|
||||||
|
return valueA.compareTo(valueB);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Erreur lors de la comparaison de la propriété: {}", property, e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les accesseurs de propriétés pour optimiser les performances.
|
||||||
|
*/
|
||||||
|
private void initializePropertyAccessors() {
|
||||||
|
columns.forEach(column -> {
|
||||||
|
String property = column.getField();
|
||||||
|
try {
|
||||||
|
Method getter = findGetter(property);
|
||||||
|
propertyAccessors.put(property, item -> getter.invoke(item));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Impossible de créer l'accesseur pour la propriété: {}", property, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trouve la méthode getter pour une propriété.
|
||||||
|
*/
|
||||||
|
private Method findGetter(String property) throws NoSuchMethodException {
|
||||||
|
String getterName = "get" + property.substring(0, 1).toUpperCase() + property.substring(1);
|
||||||
|
return data.get(0).getClass().getMethod(getterName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface fonctionnelle pour l'accès aux propriétés.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface PropertyAccessor<T> {
|
||||||
|
Object access(T item) throws Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute un trieur personnalisé pour une colonne.
|
||||||
|
*/
|
||||||
|
public void addCustomSorter(String property, Comparator<T> comparator) {
|
||||||
|
customSorters.put(property, comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le nombre total de lignes.
|
||||||
|
*/
|
||||||
|
private void updateRowCount() {
|
||||||
|
if (lazyModel != null) {
|
||||||
|
lazyModel.setRowCount(data.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère l'événement de changement de page.
|
||||||
|
*/
|
||||||
|
public void onPageChange(PageEvent event) {
|
||||||
|
log.debug("Changement de page: {}", event.getPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide les paramètres d'initialisation.
|
||||||
|
*/
|
||||||
|
private void validateInitializationParameters(List<T> data, List<Column> columns) {
|
||||||
|
if (data == null || data.isEmpty()) {
|
||||||
|
throw new DataTableException("Les données ne peuvent pas être nulles ou vides");
|
||||||
|
}
|
||||||
|
if (columns == null || columns.isEmpty()) {
|
||||||
|
throw new DataTableException("La configuration des colonnes est requise");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rafraîchit les données du tableau.
|
||||||
|
*/
|
||||||
|
public void refresh() {
|
||||||
|
log.debug("Rafraîchissement du tableau");
|
||||||
|
updateRowCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object getPropertyValue(T item, String property) throws Exception {
|
||||||
|
PropertyAccessor<T> accessor = propertyAccessors.get(property);
|
||||||
|
if (accessor != null) {
|
||||||
|
return accessor.access(item);
|
||||||
|
}
|
||||||
|
throw new NoSuchFieldException("Propriété inaccessible : " + property);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
226
src/main/java/dev/lions/components/FileUploadComponent.java
Normal file
226
src/main/java/dev/lions/components/FileUploadComponent.java
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
package dev.lions.components;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
import jakarta.faces.application.FacesMessage;
|
||||||
|
import jakarta.faces.context.FacesContext;
|
||||||
|
import jakarta.faces.view.ViewScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Named;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.primefaces.event.FileUploadEvent;
|
||||||
|
import org.primefaces.model.file.UploadedFile;
|
||||||
|
|
||||||
|
import dev.lions.config.ApplicationConfig;
|
||||||
|
import dev.lions.exceptions.FileUploadException;
|
||||||
|
import dev.lions.services.FileStorageService;
|
||||||
|
import dev.lions.utils.FileValidator;
|
||||||
|
import dev.lions.utils.SecurityUtils;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composant de gestion des téléchargements de fichiers.
|
||||||
|
* Fournit une interface sécurisée et performante pour le téléchargement,
|
||||||
|
* la validation et la gestion des fichiers dans l'application.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 2.1
|
||||||
|
*/
|
||||||
|
@Named
|
||||||
|
@ViewScoped
|
||||||
|
@Slf4j
|
||||||
|
public class FileUploadComponent implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private static final int MAX_FILES = 10; // Limite de fichiers autorisés
|
||||||
|
private static final String TEMP_DIR_PREFIX = "upload_"; // Préfixe pour répertoire temporaire
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ApplicationConfig appConfig;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
FileStorageService storageService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
FileValidator fileValidator;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityUtils securityUtils;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final List<UploadedFileInfo> uploadedFiles = new ArrayList<>();
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private String uploadDirectory;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private boolean multiple = false;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private String acceptedTypes;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private long maxFileSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisation du composant.
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
this.maxFileSize = appConfig.getMaxFileSize();
|
||||||
|
this.acceptedTypes = appConfig.getAllowedFileTypes();
|
||||||
|
this.uploadDirectory = createTempUploadDirectory();
|
||||||
|
|
||||||
|
log.info("Composant de téléchargement initialisé. Taille max: {}, Types acceptés: {}",
|
||||||
|
maxFileSize, acceptedTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère l'événement de téléchargement de fichier.
|
||||||
|
*
|
||||||
|
* @param event L'événement PrimeFaces contenant le fichier téléchargé.
|
||||||
|
*/
|
||||||
|
public void handleFileUpload(@NotNull FileUploadEvent event) {
|
||||||
|
UploadedFile file = event.getFile();
|
||||||
|
log.info("Téléchargement de fichier : {}", file.getFileName());
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateUploadRequest(file);
|
||||||
|
UploadedFileInfo fileInfo = processUploadedFile(file);
|
||||||
|
uploadedFiles.add(fileInfo);
|
||||||
|
|
||||||
|
addSuccessMessage("Fichier téléchargé avec succès : " + fileInfo.getFileName());
|
||||||
|
} catch (FileUploadException e) {
|
||||||
|
log.error("Erreur de validation du fichier : {}", file.getFileName(), e);
|
||||||
|
addErrorMessage(e.getMessage());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Erreur lors du traitement du fichier : {}", file.getFileName(), e);
|
||||||
|
addErrorMessage("Une erreur est survenue lors du traitement du fichier.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide la requête de téléchargement.
|
||||||
|
*
|
||||||
|
* @param file Le fichier téléchargé.
|
||||||
|
*/
|
||||||
|
private void validateUploadRequest(UploadedFile file) {
|
||||||
|
if (uploadedFiles.size() >= MAX_FILES) {
|
||||||
|
throw new FileUploadException("Vous avez atteint le nombre maximum de fichiers autorisés.");
|
||||||
|
}
|
||||||
|
fileValidator.validateFile(file, acceptedTypes, maxFileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traite et stocke le fichier téléchargé.
|
||||||
|
*
|
||||||
|
* @param file Le fichier téléchargé.
|
||||||
|
* @return Les informations du fichier.
|
||||||
|
* @throws IOException En cas d'erreur de stockage.
|
||||||
|
*/
|
||||||
|
private UploadedFileInfo processUploadedFile(UploadedFile file) throws IOException {
|
||||||
|
String secureFileName = generateSecureFileName(file.getFileName());
|
||||||
|
Path destinationPath = storageService.storeFile(file.getInputStream(), uploadDirectory, secureFileName);
|
||||||
|
|
||||||
|
return UploadedFileInfo.builder()
|
||||||
|
.id(UUID.randomUUID().toString())
|
||||||
|
.fileName(file.getFileName())
|
||||||
|
.contentType(file.getContentType())
|
||||||
|
.size(file.getSize())
|
||||||
|
.path(destinationPath)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un nom de fichier sécurisé.
|
||||||
|
*
|
||||||
|
* @param originalFileName Nom original.
|
||||||
|
* @return Nom sécurisé.
|
||||||
|
*/
|
||||||
|
private String generateSecureFileName(String originalFileName) {
|
||||||
|
String extension = getFileExtension(originalFileName);
|
||||||
|
return securityUtils.sanitizeFileName(UUID.randomUUID().toString() + "." + extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'extension d'un fichier.
|
||||||
|
*
|
||||||
|
* @param fileName Nom du fichier.
|
||||||
|
* @return Extension.
|
||||||
|
*/
|
||||||
|
private String getFileExtension(String fileName) {
|
||||||
|
return fileName.substring(fileName.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un répertoire temporaire pour les téléchargements.
|
||||||
|
*
|
||||||
|
* @return Le chemin du répertoire temporaire.
|
||||||
|
*/
|
||||||
|
private String createTempUploadDirectory() {
|
||||||
|
return storageService.createTempDirectory(TEMP_DIR_PREFIX + UUID.randomUUID());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie les ressources lors de la destruction du composant.
|
||||||
|
*/
|
||||||
|
@PreDestroy
|
||||||
|
public void cleanup() {
|
||||||
|
try {
|
||||||
|
storageService.deleteDirectory(uploadDirectory);
|
||||||
|
log.info("Répertoire temporaire supprimé : {}", uploadDirectory);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors du nettoyage des ressources : {}", uploadDirectory, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute un message de succès dans l'interface utilisateur.
|
||||||
|
*
|
||||||
|
* @param message Message à afficher.
|
||||||
|
*/
|
||||||
|
private void addSuccessMessage(String message) {
|
||||||
|
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute un message d'erreur dans l'interface utilisateur.
|
||||||
|
*
|
||||||
|
* @param message Message à afficher.
|
||||||
|
*/
|
||||||
|
private void addErrorMessage(String message) {
|
||||||
|
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classe interne représentant un fichier téléchargé.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class UploadedFileInfo {
|
||||||
|
private final String id;
|
||||||
|
private final String fileName;
|
||||||
|
private final String contentType;
|
||||||
|
private final long size;
|
||||||
|
private final Path path;
|
||||||
|
}
|
||||||
|
}
|
||||||
225
src/main/java/dev/lions/components/FilterComponent.java
Normal file
225
src/main/java/dev/lions/components/FilterComponent.java
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package dev.lions.components;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.faces.model.SelectItem;
|
||||||
|
import jakarta.faces.view.ViewScoped;
|
||||||
|
import jakarta.inject.Named;
|
||||||
|
import jakarta.faces.context.FacesContext;
|
||||||
|
import jakarta.faces.application.FacesMessage;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import dev.lions.utils.FilterOperator;
|
||||||
|
import dev.lions.utils.FilterCriteria;
|
||||||
|
import dev.lions.exceptions.FilterException;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composant de gestion des filtres dynamiques.
|
||||||
|
* Permet la création, la validation et l'application de filtres
|
||||||
|
* pour des tableaux de données.
|
||||||
|
*
|
||||||
|
* <p>Fonctionnalités incluses :
|
||||||
|
* <ul>
|
||||||
|
* <li>Ajout de filtres avec validation des entrées</li>
|
||||||
|
* <li>Suppression de filtres</li>
|
||||||
|
* <li>Application des filtres sur des listes d'objets</li>
|
||||||
|
* <li>Interface utilisateur avec feedback via les messages JSF</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 2.2
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Named
|
||||||
|
@ViewScoped
|
||||||
|
public class FilterComponent implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private static final int MAX_FILTERS = 10;
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
private List<FilterCriteria> criteria = new ArrayList<>();
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
private String selectedField;
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
private FilterOperator selectedOperator;
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
private String filterValue;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private List<SelectItem> availableFields;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private List<SelectItem> availableOperators;
|
||||||
|
|
||||||
|
private final Map<String, String> fieldConfigurations = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
log.debug("Initialisation du composant de filtrage");
|
||||||
|
initializeFieldConfigurations();
|
||||||
|
initializeAvailableFields();
|
||||||
|
initializeAvailableOperators();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise la configuration des champs disponibles.
|
||||||
|
*/
|
||||||
|
private void initializeFieldConfigurations() {
|
||||||
|
fieldConfigurations.put("name", "Nom");
|
||||||
|
fieldConfigurations.put("date", "Date");
|
||||||
|
fieldConfigurations.put("status", "Statut");
|
||||||
|
fieldConfigurations.put("category", "Catégorie");
|
||||||
|
fieldConfigurations.put("price", "Prix");
|
||||||
|
log.info("Champs disponibles pour le filtrage : {}", fieldConfigurations.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remplit la liste des champs disponibles.
|
||||||
|
*/
|
||||||
|
private void initializeAvailableFields() {
|
||||||
|
availableFields = new ArrayList<>();
|
||||||
|
fieldConfigurations.forEach((key, value) ->
|
||||||
|
availableFields.add(new SelectItem(key, value))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remplit la liste des opérateurs disponibles.
|
||||||
|
*/
|
||||||
|
private void initializeAvailableOperators() {
|
||||||
|
availableOperators = new ArrayList<>();
|
||||||
|
for (FilterOperator operator : FilterOperator.values()) {
|
||||||
|
availableOperators.add(new SelectItem(operator, operator.getLabel()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute un critère de filtrage après validation.
|
||||||
|
*/
|
||||||
|
public void addFilter() {
|
||||||
|
log.debug("Ajout d'un filtre : Champ = {}, Opérateur = {}, Valeur = {}",
|
||||||
|
selectedField, selectedOperator, filterValue);
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateFilterInput();
|
||||||
|
validateFilterLimit();
|
||||||
|
|
||||||
|
FilterCriteria newCriteria = new FilterCriteria(selectedField, selectedOperator, filterValue);
|
||||||
|
criteria.add(newCriteria);
|
||||||
|
|
||||||
|
addMessage(FacesMessage.SEVERITY_INFO, "Filtre ajouté", "Filtre appliqué avec succès.");
|
||||||
|
log.info("Filtre ajouté avec succès : {}", newCriteria);
|
||||||
|
resetForm();
|
||||||
|
} catch (FilterException e) {
|
||||||
|
log.warn("Erreur de validation du filtre", e);
|
||||||
|
addMessage(FacesMessage.SEVERITY_ERROR, "Erreur", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un critère de filtrage.
|
||||||
|
*
|
||||||
|
* @param filter Le critère à supprimer.
|
||||||
|
*/
|
||||||
|
public void removeFilter(@NotNull FilterCriteria filter) {
|
||||||
|
log.debug("Suppression du filtre : {}", filter);
|
||||||
|
criteria.remove(filter);
|
||||||
|
addMessage(FacesMessage.SEVERITY_INFO, "Filtre supprimé", "Le filtre a été retiré.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Efface tous les filtres existants.
|
||||||
|
*/
|
||||||
|
public void clearAllFilters() {
|
||||||
|
log.info("Suppression de tous les filtres ({})", criteria.size());
|
||||||
|
criteria.clear();
|
||||||
|
addMessage(FacesMessage.SEVERITY_INFO, "Filtres effacés", "Tous les filtres ont été supprimés.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applique les filtres sur une liste de données.
|
||||||
|
*
|
||||||
|
* @param data Liste des objets à filtrer.
|
||||||
|
* @return Liste filtrée.
|
||||||
|
*/
|
||||||
|
public List<Object> applyFilters(List<Object> data) {
|
||||||
|
if (criteria.isEmpty()) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
log.debug("Application des filtres sur {} éléments", data.size());
|
||||||
|
return data.stream().filter(this::matchesAllCriteria).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide les entrées du filtre.
|
||||||
|
*/
|
||||||
|
private void validateFilterInput() {
|
||||||
|
if (selectedField == null || selectedOperator == null || filterValue == null) {
|
||||||
|
throw new FilterException("Tous les champs du filtre doivent être remplis.");
|
||||||
|
}
|
||||||
|
if (selectedOperator.isNumericComparison()) {
|
||||||
|
try {
|
||||||
|
Double.parseDouble(filterValue);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new FilterException("La valeur doit être numérique pour cet opérateur.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateFilterLimit() {
|
||||||
|
if (criteria.size() >= MAX_FILTERS) {
|
||||||
|
throw new FilterException("Nombre maximum de filtres atteint (" + MAX_FILTERS + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesAllCriteria(Object item) {
|
||||||
|
return criteria.stream().allMatch(filter -> matchesCriteria(item, filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesCriteria(Object item, FilterCriteria filter) {
|
||||||
|
try {
|
||||||
|
Object value = getPropertyValue(item, filter.getField());
|
||||||
|
return filter.getOperator().apply(value, (String) filter.getValue());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Erreur d'accès à la propriété : {}", filter.getField(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object getPropertyValue(Object item, String property) {
|
||||||
|
try {
|
||||||
|
Method getter = item.getClass().getMethod("get" + capitalize(property));
|
||||||
|
return getter.invoke(item);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FilterException("Propriété inaccessible : " + property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String capitalize(String str) {
|
||||||
|
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetForm() {
|
||||||
|
selectedField = null;
|
||||||
|
selectedOperator = null;
|
||||||
|
filterValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addMessage(FacesMessage.Severity severity, String summary, String detail) {
|
||||||
|
FacesContext.getCurrentInstance()
|
||||||
|
.addMessage(null, new FacesMessage(severity, summary, detail));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package dev.lions.components;
|
||||||
|
|
||||||
|
import jakarta.faces.application.FacesMessage;
|
||||||
|
import jakarta.faces.context.FacesContext;
|
||||||
|
import jakarta.faces.view.ViewScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Named;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composant gérant l'affichage des notifications dans l'interface utilisateur.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Named
|
||||||
|
@ViewScoped
|
||||||
|
public class NotificationComponent implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private static final String MESSAGE_BUNDLE = "messages";
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
FacesContext facesContext;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
transient ResourceBundle messageBundle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Affiche un message de succès.
|
||||||
|
*/
|
||||||
|
public void showSuccess(@NotBlank String key) {
|
||||||
|
log.debug("Affichage message succès: {}", key);
|
||||||
|
addMessage(FacesMessage.SEVERITY_INFO,
|
||||||
|
getMessage(key + ".title", "Succès"),
|
||||||
|
getMessage(key + ".detail"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Affiche un message d'erreur.
|
||||||
|
*/
|
||||||
|
public void showError(@NotBlank String key) {
|
||||||
|
log.debug("Affichage message erreur: {}", key);
|
||||||
|
addMessage(FacesMessage.SEVERITY_ERROR,
|
||||||
|
getMessage(key + ".title", "Erreur"),
|
||||||
|
getMessage(key + ".detail"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Affiche un message d'avertissement.
|
||||||
|
*/
|
||||||
|
public void showWarning(@NotBlank String key) {
|
||||||
|
log.debug("Affichage message avertissement: {}", key);
|
||||||
|
addMessage(FacesMessage.SEVERITY_WARN,
|
||||||
|
getMessage(key + ".title", "Attention"),
|
||||||
|
getMessage(key + ".detail"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un message localisé avec fallback.
|
||||||
|
*/
|
||||||
|
private String getMessage(String key, String defaultValue) {
|
||||||
|
try {
|
||||||
|
return messageBundle.getString(key);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Message non trouvé: {}", key);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMessage(String key) {
|
||||||
|
return getMessage(key, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addMessage(FacesMessage.Severity severity, String summary, String detail) {
|
||||||
|
facesContext.addMessage(null, new FacesMessage(severity, summary, detail));
|
||||||
|
log.debug("Message ajouté: {} - {}", summary, detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
339
src/main/java/dev/lions/config/ApplicationConfig.java
Normal file
339
src/main/java/dev/lions/config/ApplicationConfig.java
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
package dev.lions.config;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
import dev.lions.exceptions.ConfigurationException;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration centrale de l'application Lions Dev.
|
||||||
|
* Cette classe gère l'ensemble des paramètres de configuration de manière thread-safe
|
||||||
|
* et fournit une interface unifiée pour accéder aux différentes configurations.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 2.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
@Getter
|
||||||
|
public class ApplicationConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Énumération des environnements d'exécution supportés.
|
||||||
|
*/
|
||||||
|
public enum Environment {
|
||||||
|
DEVELOPMENT("development"),
|
||||||
|
STAGING("staging"),
|
||||||
|
PRODUCTION("production");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
Environment(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Environment fromString(String value) {
|
||||||
|
return Arrays.stream(values())
|
||||||
|
.filter(env -> env.getValue().equalsIgnoreCase(value))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(DEVELOPMENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constantes de configuration
|
||||||
|
private static final String DEFAULT_ENVIRONMENT = "development";
|
||||||
|
private static final long DEFAULT_MAX_FILE_SIZE = 10_485_760L; // 10MB
|
||||||
|
private static final int DEFAULT_CACHE_SIZE = 1000;
|
||||||
|
private static final int MIN_PORT = 1;
|
||||||
|
private static final int MAX_PORT = 65535;
|
||||||
|
|
||||||
|
// Configuration de base de l'application
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.name", defaultValue = "Lions Dev")
|
||||||
|
private String applicationName;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.environment", defaultValue = DEFAULT_ENVIRONMENT)
|
||||||
|
private String environment;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.base-url")
|
||||||
|
private String baseUrl;
|
||||||
|
|
||||||
|
// Configuration du stockage
|
||||||
|
@NotBlank
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.storage.base-path")
|
||||||
|
private String storageBasePath;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.storage.images.path", defaultValue = "images")
|
||||||
|
private String imageStoragePath;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.storage.allowed-types", defaultValue = "jpg,jpeg,png,gif")
|
||||||
|
private String allowedFileTypes;
|
||||||
|
|
||||||
|
@Min(1_048_576L) // 1MB minimum
|
||||||
|
@Max(104_857_600L) // 100MB maximum
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.storage.max-size", defaultValue = "10485760")
|
||||||
|
private Long maxFileSize;
|
||||||
|
|
||||||
|
// Configuration des emails
|
||||||
|
@NotBlank
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.email.from")
|
||||||
|
private String emailFrom;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.email.support")
|
||||||
|
private String emailSupport;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.email.template-path", defaultValue = "templates/email")
|
||||||
|
private String emailTemplatePath;
|
||||||
|
|
||||||
|
// Configuration SMTP
|
||||||
|
@NotBlank
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.smtp.host")
|
||||||
|
private String smtpHost;
|
||||||
|
|
||||||
|
@Min(MIN_PORT)
|
||||||
|
@Max(MAX_PORT)
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.smtp.port")
|
||||||
|
private Integer smtpPort;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.smtp.username")
|
||||||
|
private Optional<String> smtpUsername;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.smtp.password")
|
||||||
|
private Optional<String> smtpPassword;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.admin.email")
|
||||||
|
private String adminEmailAddress;
|
||||||
|
|
||||||
|
// Collections thread-safe pour les configurations dynamiques
|
||||||
|
private final Map<String, String> applicationUrls = new ConcurrentHashMap<>();
|
||||||
|
private final Map<Environment, String> environmentConfigs = new EnumMap<>(Environment.class);
|
||||||
|
private List<String> allowedFileTypesList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise la configuration après l'injection des propriétés.
|
||||||
|
* Valide et prépare l'ensemble des paramètres de configuration.
|
||||||
|
*
|
||||||
|
* @throws ConfigurationException si la configuration est invalide
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
void initialize() {
|
||||||
|
try {
|
||||||
|
log.info("Initialisation de la configuration de l'application: {}", applicationName);
|
||||||
|
validateConfiguration();
|
||||||
|
initializeApplicationUrls();
|
||||||
|
initializeAllowedFileTypes();
|
||||||
|
initializeEnvironmentConfigs();
|
||||||
|
log.info("Configuration initialisée avec succès en environnement: {}", environment);
|
||||||
|
} catch (Exception e) {
|
||||||
|
String errorMessage = "Erreur lors de l'initialisation de la configuration";
|
||||||
|
log.error(errorMessage, e);
|
||||||
|
throw new ConfigurationException(errorMessage, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide l'ensemble de la configuration.
|
||||||
|
*
|
||||||
|
* @throws ConfigurationException si la validation échoue
|
||||||
|
*/
|
||||||
|
private void validateConfiguration() {
|
||||||
|
log.debug("Validation de la configuration");
|
||||||
|
validateEnvironment();
|
||||||
|
validateStoragePaths();
|
||||||
|
validateSmtpConfiguration();
|
||||||
|
validateFileSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide l'environnement d'exécution.
|
||||||
|
*/
|
||||||
|
private void validateEnvironment() {
|
||||||
|
if (!isValidEnvironment(environment)) {
|
||||||
|
throw new ConfigurationException("Environnement non reconnu: " + environment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide les chemins de stockage.
|
||||||
|
*/
|
||||||
|
private void validateStoragePaths() {
|
||||||
|
Path basePath = Paths.get(storageBasePath);
|
||||||
|
validatePath(basePath, "stockage principal");
|
||||||
|
|
||||||
|
Path imagesPath = basePath.resolve(imageStoragePath);
|
||||||
|
validatePath(imagesPath, "stockage des images");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide un chemin spécifique.
|
||||||
|
*/
|
||||||
|
private void validatePath(Path path, String description) {
|
||||||
|
if (!path.toFile().exists() && !path.toFile().mkdirs()) {
|
||||||
|
throw new ConfigurationException(
|
||||||
|
"Impossible de créer le répertoire de " + description + ": " + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide la configuration SMTP.
|
||||||
|
*/
|
||||||
|
private void validateSmtpConfiguration() {
|
||||||
|
if (isSmtpConfigured() && (smtpPort < MIN_PORT || smtpPort > MAX_PORT)) {
|
||||||
|
throw new ConfigurationException("Port SMTP invalide: " + smtpPort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide la taille maximale des fichiers.
|
||||||
|
*/
|
||||||
|
private void validateFileSize() {
|
||||||
|
if (maxFileSize <= 0) {
|
||||||
|
throw new ConfigurationException(
|
||||||
|
"Taille maximale de fichier invalide: " + maxFileSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'adresse email système (expéditeur par défaut).
|
||||||
|
*
|
||||||
|
* @return Adresse email système
|
||||||
|
*/
|
||||||
|
public String getSystemEmailAddress() {
|
||||||
|
return emailFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le SSL est activé pour le serveur SMTP.
|
||||||
|
*
|
||||||
|
* @return true si SSL est activé, sinon false
|
||||||
|
*/
|
||||||
|
public boolean isSmtpSslEnabled() {
|
||||||
|
return smtpPort == 465; // Port 465 est commun pour SMTP avec SSL
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les URLs de l'application.
|
||||||
|
*/
|
||||||
|
private void initializeApplicationUrls() {
|
||||||
|
applicationUrls.clear();
|
||||||
|
applicationUrls.put("home", "/");
|
||||||
|
applicationUrls.put("services", "/services");
|
||||||
|
applicationUrls.put("contact", "/contact");
|
||||||
|
applicationUrls.put("admin", "/admin");
|
||||||
|
applicationUrls.put("projects", "/projects");
|
||||||
|
applicationUrls.put("portfolio", "/portfolio");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise la liste des types de fichiers autorisés.
|
||||||
|
*/
|
||||||
|
private void initializeAllowedFileTypes() {
|
||||||
|
allowedFileTypesList = Collections.unmodifiableList(
|
||||||
|
Arrays.asList(allowedFileTypes.toLowerCase().split(","))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les configurations spécifiques aux environnements.
|
||||||
|
*/
|
||||||
|
private void initializeEnvironmentConfigs() {
|
||||||
|
environmentConfigs.put(Environment.DEVELOPMENT, "dev");
|
||||||
|
environmentConfigs.put(Environment.STAGING, "stage");
|
||||||
|
environmentConfigs.put(Environment.PRODUCTION, "prod");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes publiques utilitaires
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le chemin complet pour le stockage des images.
|
||||||
|
*/
|
||||||
|
public String getImageStoragePath() {
|
||||||
|
return Paths.get(storageBasePath, imageStoragePath).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un type de fichier est autorisé.
|
||||||
|
*/
|
||||||
|
public boolean isFileTypeAllowed(String fileType) {
|
||||||
|
return fileType != null && allowedFileTypesList.contains(fileType.toLowerCase().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'URL d'une section de l'application.
|
||||||
|
*/
|
||||||
|
public String getUrl(String key) {
|
||||||
|
return applicationUrls.getOrDefault(key, "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'environnement est en développement.
|
||||||
|
*/
|
||||||
|
public boolean isDevelopment() {
|
||||||
|
return Environment.DEVELOPMENT.getValue().equals(environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'environnement est en production.
|
||||||
|
*/
|
||||||
|
public boolean isProduction() {
|
||||||
|
return Environment.PRODUCTION.getValue().equals(environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si la configuration SMTP est complète.
|
||||||
|
*/
|
||||||
|
public boolean isSmtpConfigured() {
|
||||||
|
return smtpUsername.isPresent() && smtpPassword.isPresent() &&
|
||||||
|
smtpHost != null && !smtpHost.equals("localhost");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un environnement est valide.
|
||||||
|
*/
|
||||||
|
private boolean isValidEnvironment(String env) {
|
||||||
|
return Arrays.stream(Environment.values())
|
||||||
|
.anyMatch(e -> e.getValue().equals(env));
|
||||||
|
}
|
||||||
|
}
|
||||||
180
src/main/java/dev/lions/config/ApplicationConfigService.java
Normal file
180
src/main/java/dev/lions/config/ApplicationConfigService.java
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package dev.lions.config;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.event.Event;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import dev.lions.events.ConfigurationEvent;
|
||||||
|
import dev.lions.exceptions.ConfigurationException;
|
||||||
|
import dev.lions.utils.EncryptionUtils;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion avancée de la configuration de l'application.
|
||||||
|
* Fournit une interface enrichie pour accéder et gérer les paramètres de configuration
|
||||||
|
* de manière thread-safe, sécurisée et optimisée.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 2.1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ApplicationConfigService {
|
||||||
|
|
||||||
|
private static final long CACHE_DURATION_MS = 300_000; // 5 minutes
|
||||||
|
private static final long MIN_DISK_SPACE_BYTES = 100 * 1024 * 1024; // 100 MB
|
||||||
|
private static final String[] MANDATORY_DIRECTORIES = {"logs", "data", "temp"};
|
||||||
|
|
||||||
|
private final ApplicationConfig applicationConfig;
|
||||||
|
private final Map<String, CachedValue<Object>> configCache;
|
||||||
|
private final AtomicLong lastHealthCheck;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Event<ConfigurationEvent> configurationEvent;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private EncryptionUtils encryptionUtils;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ApplicationConfigService(@NotNull ApplicationConfig applicationConfig) {
|
||||||
|
this.applicationConfig = applicationConfig;
|
||||||
|
this.configCache = new ConcurrentHashMap<>();
|
||||||
|
this.lastHealthCheck = new AtomicLong(0);
|
||||||
|
|
||||||
|
initializeService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide la configuration actuelle de l'application.
|
||||||
|
* Vérifie les chemins de stockage, les répertoires obligatoires et les paramètres critiques.
|
||||||
|
*/
|
||||||
|
private void validateConfiguration() {
|
||||||
|
log.info("Validation de la configuration de l'application...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateStorageBasePath();
|
||||||
|
validateMandatoryDirectories();
|
||||||
|
validateSecuritySettings();
|
||||||
|
|
||||||
|
log.info("Configuration de l'application validée avec succès.");
|
||||||
|
} catch (ConfigurationException e) {
|
||||||
|
log.error("Validation échouée : {}", e.getMessage());
|
||||||
|
throw e; // Relancer l'exception après log
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur inattendue lors de la validation de la configuration", e);
|
||||||
|
throw new ConfigurationException("Erreur inattendue lors de la validation", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie l'existence et l'accessibilité du chemin de stockage.
|
||||||
|
*/
|
||||||
|
private void validateStorageBasePath() {
|
||||||
|
Path storagePath = Paths.get(applicationConfig.getStorageBasePath());
|
||||||
|
if (!Files.exists(storagePath) || !Files.isDirectory(storagePath)) {
|
||||||
|
throw new ConfigurationException("Le chemin de stockage est invalide : " + storagePath);
|
||||||
|
}
|
||||||
|
log.debug("Chemin de stockage validé : {}", storagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie l'existence des répertoires obligatoires.
|
||||||
|
*/
|
||||||
|
private void validateMandatoryDirectories() {
|
||||||
|
String basePath = applicationConfig.getStorageBasePath();
|
||||||
|
for (String directory : MANDATORY_DIRECTORIES) {
|
||||||
|
Path directoryPath = Paths.get(basePath, directory);
|
||||||
|
if (!Files.exists(directoryPath) || !Files.isWritable(directoryPath)) {
|
||||||
|
throw new ConfigurationException("Répertoire obligatoire manquant ou inaccessible : " + directoryPath);
|
||||||
|
}
|
||||||
|
log.debug("Répertoire valide : {}", directoryPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide les paramètres de sécurité essentiels.
|
||||||
|
*/
|
||||||
|
private void validateSecuritySettings() {
|
||||||
|
if (applicationConfig.isProduction()) {
|
||||||
|
if (!applicationConfig.isSmtpConfigured()) {
|
||||||
|
throw new ConfigurationException("La configuration SMTP est obligatoire en production");
|
||||||
|
}
|
||||||
|
log.debug("Paramètres SMTP validés pour l'environnement production");
|
||||||
|
}
|
||||||
|
log.debug("Paramètres de sécurité validés");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeService() {
|
||||||
|
try {
|
||||||
|
log.info("Initialisation du service de configuration");
|
||||||
|
|
||||||
|
createMandatoryDirectories();
|
||||||
|
validateConfiguration();
|
||||||
|
initializeCache();
|
||||||
|
|
||||||
|
notifyServiceInitialized();
|
||||||
|
|
||||||
|
log.info("Service de configuration initialisé avec succès");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de l'initialisation du service de configuration", e);
|
||||||
|
throw new ConfigurationException("Échec de l'initialisation du service", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createMandatoryDirectories() {
|
||||||
|
String basePath = applicationConfig.getStorageBasePath();
|
||||||
|
|
||||||
|
for (String directory : MANDATORY_DIRECTORIES) {
|
||||||
|
Path directoryPath = Paths.get(basePath, directory);
|
||||||
|
if (!Files.exists(directoryPath)) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(directoryPath);
|
||||||
|
log.debug("Répertoire créé : {}", directoryPath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new ConfigurationException("Impossible de créer le répertoire : " + directoryPath, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeCache() {
|
||||||
|
log.info("Initialisation du cache de configuration...");
|
||||||
|
configCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyServiceInitialized() {
|
||||||
|
ConfigurationEvent event = new ConfigurationEvent(
|
||||||
|
"SERVICE_INITIALIZED",
|
||||||
|
Map.of("timestamp", System.currentTimeMillis(),
|
||||||
|
"environment", applicationConfig.getEnvironment())
|
||||||
|
);
|
||||||
|
configurationEvent.fire(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classe interne représentant une valeur mise en cache.
|
||||||
|
*/
|
||||||
|
private static class CachedValue<T> {
|
||||||
|
private final T value;
|
||||||
|
private final long timestamp;
|
||||||
|
|
||||||
|
CachedValue(T value) {
|
||||||
|
this.value = value;
|
||||||
|
this.timestamp = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
T getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
263
src/main/java/dev/lions/config/ElasticsearchConfig.java
Normal file
263
src/main/java/dev/lions/config/ElasticsearchConfig.java
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
package dev.lions.config;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
import dev.lions.exceptions.ConfigurationException;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration Elasticsearch de l'application.
|
||||||
|
* Cette classe gère l'ensemble des paramètres de configuration pour l'intégration
|
||||||
|
* Elasticsearch de manière thread-safe et optimisée.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 2.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
@Getter
|
||||||
|
public class ElasticsearchConfig {
|
||||||
|
|
||||||
|
private static final int DEFAULT_SHARDS = 5;
|
||||||
|
private static final int DEFAULT_REPLICAS = 1;
|
||||||
|
private static final String INDEX_PREFIX = "lions_";
|
||||||
|
private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30);
|
||||||
|
private static final int MAX_RETRY_COUNT = 3;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "app.environment")
|
||||||
|
private String environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration des connexions Elasticsearch.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public static class ConnectionConfig {
|
||||||
|
@NotNull
|
||||||
|
private String hosts;
|
||||||
|
|
||||||
|
@Min(1)
|
||||||
|
@Max(65535)
|
||||||
|
private Integer port = 9200;
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
private Boolean useSsl = false;
|
||||||
|
|
||||||
|
@Min(1000)
|
||||||
|
private Integer connectionTimeout = 5000;
|
||||||
|
|
||||||
|
@Min(1000)
|
||||||
|
private Integer socketTimeout = 60000;
|
||||||
|
|
||||||
|
@Min(1000)
|
||||||
|
private Integer maxRetryTimeout = 60000;
|
||||||
|
|
||||||
|
private Map<String, String> additionalSettings = new HashMap<>();
|
||||||
|
|
||||||
|
public void validate() {
|
||||||
|
if (hosts == null || hosts.trim().isEmpty()) {
|
||||||
|
throw new ConfigurationException("La configuration des hôtes Elasticsearch est requise");
|
||||||
|
}
|
||||||
|
if (port < 1 || port > 65535) {
|
||||||
|
throw new ConfigurationException("Port invalide : " + port);
|
||||||
|
}
|
||||||
|
if (useSsl && (username == null || password == null)) {
|
||||||
|
throw new ConfigurationException("Les identifiants sont requis en mode SSL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration des index Elasticsearch.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public static class IndexConfig {
|
||||||
|
private final Map<String, IndexSettings> indices = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Object> defaultSettings = new HashMap<>();
|
||||||
|
private final Map<String, Object> analysisSettings = new HashMap<>();
|
||||||
|
|
||||||
|
public IndexConfig() {
|
||||||
|
initializeDefaultSettings();
|
||||||
|
initializeAnalysisSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeDefaultSettings() {
|
||||||
|
defaultSettings.put("number_of_shards", DEFAULT_SHARDS);
|
||||||
|
defaultSettings.put("number_of_replicas", DEFAULT_REPLICAS);
|
||||||
|
defaultSettings.put("refresh_interval", "1s");
|
||||||
|
defaultSettings.put("max_result_window", 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeAnalysisSettings() {
|
||||||
|
Map<String, Object> analyzer = new HashMap<>();
|
||||||
|
analyzer.put("type", "custom");
|
||||||
|
analyzer.put("tokenizer", "standard");
|
||||||
|
analyzer.put("filter", List.of("lowercase", "asciifolding"));
|
||||||
|
|
||||||
|
analysisSettings.put("analyzer", Map.of("default_analyzer", analyzer));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addIndex(String name, IndexSettings settings) {
|
||||||
|
indices.put(name, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndexSettings getIndexSettings(String name) {
|
||||||
|
return indices.getOrDefault(name, createDefaultSettings());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndexSettings createDefaultSettings() {
|
||||||
|
return new IndexSettings(DEFAULT_SHARDS, DEFAULT_REPLICAS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paramètres spécifiques à un index.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public static class IndexSettings {
|
||||||
|
@Min(1)
|
||||||
|
@Max(50)
|
||||||
|
private final int numberOfShards;
|
||||||
|
|
||||||
|
@Min(0)
|
||||||
|
@Max(5)
|
||||||
|
private final int numberOfReplicas;
|
||||||
|
|
||||||
|
private final Map<String, Object> customSettings;
|
||||||
|
|
||||||
|
public IndexSettings(int numberOfShards, int numberOfReplicas) {
|
||||||
|
this.numberOfShards = numberOfShards;
|
||||||
|
this.numberOfReplicas = numberOfReplicas;
|
||||||
|
this.customSettings = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCustomSetting(String key, Object value) {
|
||||||
|
customSettings.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ConnectionConfig connectionConfig = new ConnectionConfig();
|
||||||
|
private final IndexConfig indexConfig = new IndexConfig();
|
||||||
|
private final Map<String, Object> clientSettings = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise la configuration Elasticsearch.
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void initialize() {
|
||||||
|
try {
|
||||||
|
log.info("Initialisation de la configuration Elasticsearch");
|
||||||
|
|
||||||
|
validateConfiguration();
|
||||||
|
initializeIndices();
|
||||||
|
configureClient();
|
||||||
|
|
||||||
|
log.info("Configuration Elasticsearch initialisée avec succès");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
String message = "Erreur lors de l'initialisation de la configuration Elasticsearch";
|
||||||
|
log.error(message, e);
|
||||||
|
throw new ConfigurationException(message, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide la configuration Elasticsearch.
|
||||||
|
*/
|
||||||
|
private void validateConfiguration() {
|
||||||
|
log.debug("Validation de la configuration Elasticsearch");
|
||||||
|
connectionConfig.validate();
|
||||||
|
validateIndices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide la configuration des indices.
|
||||||
|
*/
|
||||||
|
private void validateIndices() {
|
||||||
|
indexConfig.getIndices().values().forEach(settings -> {
|
||||||
|
if (settings.getNumberOfShards() < 1 || settings.getNumberOfShards() > 50) {
|
||||||
|
throw new ConfigurationException(
|
||||||
|
"Nombre de shards invalide : " + settings.getNumberOfShards());
|
||||||
|
}
|
||||||
|
if (settings.getNumberOfReplicas() < 0 || settings.getNumberOfReplicas() > 5) {
|
||||||
|
throw new ConfigurationException(
|
||||||
|
"Nombre de réplicas invalide : " + settings.getNumberOfReplicas());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les indices avec leurs configurations spécifiques.
|
||||||
|
*/
|
||||||
|
private void initializeIndices() {
|
||||||
|
String prefix = getEnvironmentPrefix();
|
||||||
|
|
||||||
|
IndexSettings projectSettings = new IndexSettings(3, isProduction() ? 2 : 1);
|
||||||
|
projectSettings.addCustomSetting("refresh_interval", isProduction() ? "30s" : "1s");
|
||||||
|
|
||||||
|
indexConfig.addIndex(prefix + "projects", projectSettings);
|
||||||
|
indexConfig.addIndex(prefix + "analytics", new IndexSettings(2, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure le client Elasticsearch.
|
||||||
|
*/
|
||||||
|
private void configureClient() {
|
||||||
|
clientSettings.put("client.transport.sniff", true);
|
||||||
|
clientSettings.put("client.transport.ignore_cluster_name", false);
|
||||||
|
clientSettings.put("client.transport.ping_timeout", "5s");
|
||||||
|
clientSettings.put("client.transport.nodes_sampler_interval", "5s");
|
||||||
|
|
||||||
|
if (isProduction()) {
|
||||||
|
enhanceProductionSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renforce les paramètres de sécurité en production.
|
||||||
|
*/
|
||||||
|
private void enhanceProductionSettings() {
|
||||||
|
clientSettings.put("xpack.security.enabled", true);
|
||||||
|
clientSettings.put("xpack.security.transport.ssl.enabled", true);
|
||||||
|
clientSettings.put("xpack.security.transport.ssl.verification_mode", "full");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le préfixe d'environnement pour les noms d'index.
|
||||||
|
*/
|
||||||
|
private String getEnvironmentPrefix() {
|
||||||
|
return isProduction() ? "prod_" : "dev_";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'environnement est en production.
|
||||||
|
*/
|
||||||
|
private boolean isProduction() {
|
||||||
|
return "production".equals(environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'URL de connexion complète.
|
||||||
|
*/
|
||||||
|
public String getConnectionUrl() {
|
||||||
|
String protocol = connectionConfig.getUseSsl() ? "https" : "http";
|
||||||
|
return String.format("%s://%s:%d", protocol, connectionConfig.getHosts(),
|
||||||
|
connectionConfig.getPort());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,183 @@
|
|||||||
package dev.lions.config;
|
package dev.lions.config;
|
||||||
|
|
||||||
|
import dev.lions.exceptions.ConfigurationException;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.event.Observes;
|
||||||
import jakarta.faces.annotation.FacesConfig;
|
import jakarta.faces.annotation.FacesConfig;
|
||||||
|
import jakarta.faces.application.ViewHandler;
|
||||||
|
import jakarta.faces.component.UIViewRoot;
|
||||||
|
import jakarta.faces.context.FacesContext;
|
||||||
|
import jakarta.faces.event.PostConstructApplicationEvent;
|
||||||
|
import jakarta.faces.event.PreDestroyApplicationEvent;
|
||||||
|
import jakarta.faces.event.SystemEvent;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration Jakarta Server Faces (JSF) de l'application.
|
||||||
|
* Cette classe gère l'ensemble des paramètres et comportements spécifiques à JSF,
|
||||||
|
* assurant une expérience utilisateur cohérente et performante.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 2.1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
@FacesConfig
|
@FacesConfig
|
||||||
public class JSFConfiguration {
|
public class JSFConfiguration {
|
||||||
}
|
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "jakarta.faces.PROJECT_STAGE", defaultValue = "Development")
|
||||||
|
String projectStage;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "jakarta.faces.FACELETS_REFRESH_PERIOD", defaultValue = "2")
|
||||||
|
Integer faceletsRefreshPeriod;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "jakarta.faces.STATE_SAVING_METHOD", defaultValue = "server")
|
||||||
|
String stateSavingMethod;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "primefaces.THEME", defaultValue = "saga")
|
||||||
|
private String primefacesTheme;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "jakarta.faces.VALIDATE_EMPTY_FIELDS", defaultValue = "true")
|
||||||
|
private Boolean validateEmptyFields;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ConfigProperty(name = "jakarta.faces.FACELETS_SKIP_COMMENTS", defaultValue = "true")
|
||||||
|
private Boolean skipComments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise la configuration JSF au démarrage de l'application.
|
||||||
|
*
|
||||||
|
* @param event Événement de construction de l'application.
|
||||||
|
*/
|
||||||
|
public void initialize(@Observes @NotNull PostConstructApplicationEvent event) {
|
||||||
|
try {
|
||||||
|
log.info("Initialisation de la configuration JSF");
|
||||||
|
|
||||||
|
configureFacesContext();
|
||||||
|
configureViewHandler();
|
||||||
|
configurePrimeFaces();
|
||||||
|
|
||||||
|
applyPerformanceOptimizations();
|
||||||
|
|
||||||
|
log.info("Configuration JSF initialisée avec succès - Mode: {}", projectStage);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
String message = "Erreur lors de l'initialisation de la configuration JSF";
|
||||||
|
log.error(message, e);
|
||||||
|
throw new ConfigurationException(message, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie les ressources JSF avant l'arrêt de l'application.
|
||||||
|
*
|
||||||
|
* @param event Événement de destruction de l'application.
|
||||||
|
*/
|
||||||
|
public void cleanup(@Observes @NotNull PreDestroyApplicationEvent event) {
|
||||||
|
log.info("Nettoyage des ressources JSF");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure le contexte Faces avec les paramètres spécifiques.
|
||||||
|
*/
|
||||||
|
private void configureFacesContext() {
|
||||||
|
log.debug("Configuration du contexte Faces");
|
||||||
|
|
||||||
|
setFacesParameter("jakarta.faces.PROJECT_STAGE", projectStage);
|
||||||
|
setFacesParameter("jakarta.faces.STATE_SAVING_METHOD", stateSavingMethod);
|
||||||
|
setFacesParameter("jakarta.faces.FACELETS_REFRESH_PERIOD", faceletsRefreshPeriod.toString());
|
||||||
|
setFacesParameter("jakarta.faces.VALIDATE_EMPTY_FIELDS", validateEmptyFields.toString());
|
||||||
|
setFacesParameter("jakarta.faces.FACELETS_SKIP_COMMENTS", skipComments.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure les paramètres spécifiques au gestionnaire de vue.
|
||||||
|
*/
|
||||||
|
private void configureViewHandler() {
|
||||||
|
log.debug("Configuration du gestionnaire de vues JSF");
|
||||||
|
|
||||||
|
FacesContext facesContext = FacesContext.getCurrentInstance();
|
||||||
|
if (facesContext == null) {
|
||||||
|
log.warn("Impossible de configurer le gestionnaire de vue : FacesContext est null.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewHandler viewHandler = facesContext.getApplication().getViewHandler();
|
||||||
|
UIViewRoot root = facesContext.getViewRoot();
|
||||||
|
|
||||||
|
if (root == null) {
|
||||||
|
root = viewHandler.createView(facesContext, "/index.xhtml");
|
||||||
|
facesContext.setViewRoot(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
root.getAttributes().put("encoding", "UTF-8");
|
||||||
|
root.getAttributes().put("contentType", "text/html");
|
||||||
|
root.getAttributes().put("characterEncoding", "UTF-8");
|
||||||
|
|
||||||
|
log.debug("Gestionnaire de vues configuré avec succès.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure les paramètres spécifiques à PrimeFaces.
|
||||||
|
*/
|
||||||
|
private void configurePrimeFaces() {
|
||||||
|
log.debug("Configuration de PrimeFaces");
|
||||||
|
|
||||||
|
setFacesParameter("primefaces.THEME", primefacesTheme);
|
||||||
|
setFacesParameter("primefaces.FONT_AWESOME", "true");
|
||||||
|
setFacesParameter("primefaces.CLIENT_SIDE_VALIDATION", "true");
|
||||||
|
setFacesParameter("primefaces.UPLOADER", "auto");
|
||||||
|
|
||||||
|
configurePrimeFacesCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure le cache PrimeFaces selon l'environnement.
|
||||||
|
*/
|
||||||
|
private void configurePrimeFacesCache() {
|
||||||
|
String cacheProvider = isDevelopmentMode() ? "memory" : "ehcache";
|
||||||
|
setFacesParameter("primefaces.CACHE_PROVIDER", cacheProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applique les optimisations de performance.
|
||||||
|
*/
|
||||||
|
private void applyPerformanceOptimizations() {
|
||||||
|
if (!isDevelopmentMode()) {
|
||||||
|
setFacesParameter("jakarta.faces.FACELETS_REFRESH_PERIOD", "-1");
|
||||||
|
setFacesParameter("jakarta.faces.COMPRESS_VIEWSTATE", "true");
|
||||||
|
setFacesParameter("jakarta.faces.PARTIAL_STATE_SAVING", "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Définit un paramètre dans le contexte Faces.
|
||||||
|
*/
|
||||||
|
private void setFacesParameter(String name, String value) {
|
||||||
|
FacesContext.getCurrentInstance()
|
||||||
|
.getExternalContext()
|
||||||
|
.getApplicationMap()
|
||||||
|
.put(name, value);
|
||||||
|
|
||||||
|
log.debug("Paramètre défini : {} = {}", name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'application est en mode développement.
|
||||||
|
*
|
||||||
|
* @return true si en mode développement, false sinon.
|
||||||
|
*/
|
||||||
|
public boolean isDevelopmentMode() {
|
||||||
|
return "Development".equals(projectStage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
264
src/main/java/dev/lions/config/StorageConfigService.java
Normal file
264
src/main/java/dev/lions/config/StorageConfigService.java
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
package dev.lions.config;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.event.Event;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import dev.lions.events.StorageEvent;
|
||||||
|
import dev.lions.exceptions.StorageConfigurationException;
|
||||||
|
import dev.lions.utils.SecurityUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.nio.file.attribute.FileAttribute;
|
||||||
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
|
import java.nio.file.attribute.PosixFilePermissions;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion avancée des configurations de stockage.
|
||||||
|
* Assure la gestion sécurisée et optimisée des paramètres de stockage fichier
|
||||||
|
* de l'application, avec validation complète et monitoring.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 2.1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class StorageConfigService {
|
||||||
|
|
||||||
|
private static final String DEFAULT_DIRECTORY_PERMISSIONS = "rwxr-x---";
|
||||||
|
private static final long MIN_FREE_SPACE_BYTES = 100 * 1024 * 1024; // 100 MB
|
||||||
|
private static final int CLEANUP_BATCH_SIZE = 100;
|
||||||
|
private static final Duration CLEANUP_INTERVAL = Duration.ofHours(24);
|
||||||
|
|
||||||
|
private final Map<String, Path> pathCache;
|
||||||
|
private final Map<String, Boolean> fileTypeCache;
|
||||||
|
private final ApplicationConfig appConfig;
|
||||||
|
private final SecurityUtils securityUtils;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Event<StorageEvent> storageEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le service avec la configuration de l'application.
|
||||||
|
*
|
||||||
|
* @param appConfig Configuration de l'application
|
||||||
|
* @param securityUtils Utilitaires de sécurité
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
public StorageConfigService(@NotNull ApplicationConfig appConfig,
|
||||||
|
@NotNull SecurityUtils securityUtils) {
|
||||||
|
this.appConfig = appConfig;
|
||||||
|
this.securityUtils = securityUtils;
|
||||||
|
this.pathCache = new ConcurrentHashMap<>();
|
||||||
|
this.fileTypeCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
initializeStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise et valide la configuration du stockage.
|
||||||
|
*/
|
||||||
|
private void initializeStorage() {
|
||||||
|
log.info("Initialisation de la configuration du stockage");
|
||||||
|
|
||||||
|
try {
|
||||||
|
createMainDirectories();
|
||||||
|
configureSecurity();
|
||||||
|
scheduleMaintenanceTasks();
|
||||||
|
validateStorageCapacity();
|
||||||
|
notifyStorageInitialized();
|
||||||
|
|
||||||
|
log.info("Configuration du stockage initialisée avec succès");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
String message = "Erreur lors de l'initialisation du stockage";
|
||||||
|
log.error(message, e);
|
||||||
|
throw new StorageConfigurationException(message, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée les répertoires principaux nécessaires au stockage.
|
||||||
|
*/
|
||||||
|
private void createMainDirectories() {
|
||||||
|
log.debug("Création des répertoires principaux de stockage");
|
||||||
|
|
||||||
|
String basePath = appConfig.getStorageBasePath();
|
||||||
|
Set<String> requiredDirs = Set.of("images", "documents", "temp", "backup");
|
||||||
|
|
||||||
|
requiredDirs.forEach(dir -> {
|
||||||
|
Path dirPath = Paths.get(basePath, dir);
|
||||||
|
createSecureDirectory(dirPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("Répertoires principaux créés avec succès.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure les paramètres de sécurité pour la production.
|
||||||
|
*/
|
||||||
|
private void applyProductionSecurity() {
|
||||||
|
log.info("Application des paramètres de sécurité spécifiques pour la production");
|
||||||
|
|
||||||
|
try {
|
||||||
|
Path basePath = Paths.get(appConfig.getStorageBasePath());
|
||||||
|
|
||||||
|
// Applique des permissions POSIX sécurisées
|
||||||
|
Set<PosixFilePermission> permissions =
|
||||||
|
PosixFilePermissions.fromString(DEFAULT_DIRECTORY_PERMISSIONS);
|
||||||
|
|
||||||
|
Files.setPosixFilePermissions(basePath, permissions);
|
||||||
|
log.info("Permissions POSIX sécurisées appliquées : {}", permissions);
|
||||||
|
|
||||||
|
// Vérifie l'accès sécurisé
|
||||||
|
if (!Files.isWritable(basePath) || !Files.isReadable(basePath)) {
|
||||||
|
throw new StorageConfigurationException(
|
||||||
|
"Permissions insuffisantes sur le répertoire de stockage : " + basePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Sécurité des répertoires en production validée");
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Erreur lors de l'application des permissions de sécurité", e);
|
||||||
|
throw new StorageConfigurationException(
|
||||||
|
"Impossible d'appliquer les paramètres de sécurité en production", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure les paramètres de sécurité pour le stockage.
|
||||||
|
*/
|
||||||
|
private void configureSecurity() {
|
||||||
|
log.info("Configuration des paramètres de sécurité du stockage");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (appConfig.isProduction()) {
|
||||||
|
applyProductionSecurity();
|
||||||
|
}
|
||||||
|
securityUtils.initializeEncryption();
|
||||||
|
log.info("Sécurité du stockage configurée avec succès");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new StorageConfigurationException("Erreur lors de la configuration de la sécurité", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Planifie les tâches de maintenance pour le stockage.
|
||||||
|
*/
|
||||||
|
private void scheduleMaintenanceTasks() {
|
||||||
|
log.info("Planification des tâches de maintenance du stockage");
|
||||||
|
|
||||||
|
try {
|
||||||
|
scheduleStorageCleanup();
|
||||||
|
scheduleCapacityCheck();
|
||||||
|
log.info("Tâches de maintenance planifiées avec succès.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new StorageConfigurationException("Erreur lors de la planification des tâches de maintenance", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un répertoire sécurisé avec les permissions appropriées.
|
||||||
|
*/
|
||||||
|
private void createSecureDirectory(Path path) {
|
||||||
|
try {
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions
|
||||||
|
.asFileAttribute(PosixFilePermissions.fromString(DEFAULT_DIRECTORY_PERMISSIONS));
|
||||||
|
Files.createDirectories(path, attr);
|
||||||
|
log.debug("Répertoire créé avec succès : {}", path);
|
||||||
|
}
|
||||||
|
validateDirectoryAccess(path);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new StorageConfigurationException(
|
||||||
|
"Impossible de créer le répertoire sécurisé : " + path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Planifie le nettoyage automatique du stockage.
|
||||||
|
*/
|
||||||
|
private void scheduleStorageCleanup() {
|
||||||
|
log.info("Planification de la tâche de nettoyage automatique du stockage");
|
||||||
|
// Simulation d'une tâche de nettoyage. Remplacer par un vrai scheduler si nécessaire.
|
||||||
|
log.debug("Nettoyage automatique exécuté toutes les {} heures, batch size: {}",
|
||||||
|
CLEANUP_INTERVAL.toHours(), CLEANUP_BATCH_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Planifie la vérification périodique de la capacité de stockage.
|
||||||
|
*/
|
||||||
|
private void scheduleCapacityCheck() {
|
||||||
|
log.info("Planification de la vérification périodique de la capacité de stockage");
|
||||||
|
// Simulation d'une vérification périodique. À remplacer par un scheduler réel.
|
||||||
|
log.debug("Vérification de la capacité planifiée toutes les {} heures", CLEANUP_INTERVAL.toHours());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide la capacité de stockage disponible.
|
||||||
|
*/
|
||||||
|
private void validateStorageCapacity() {
|
||||||
|
try {
|
||||||
|
Path storagePath = Paths.get(appConfig.getStorageBasePath());
|
||||||
|
long freeSpace = Files.getFileStore(storagePath).getUsableSpace();
|
||||||
|
|
||||||
|
if (freeSpace < MIN_FREE_SPACE_BYTES) {
|
||||||
|
throw new StorageConfigurationException(
|
||||||
|
"Espace de stockage insuffisant. Minimum requis : " +
|
||||||
|
formatSize(MIN_FREE_SPACE_BYTES));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Espace de stockage validé : {} disponible", formatSize(freeSpace));
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new StorageConfigurationException(
|
||||||
|
"Impossible de vérifier l'espace de stockage disponible", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formate une taille en bytes en format lisible.
|
||||||
|
*/
|
||||||
|
private String formatSize(long bytes) {
|
||||||
|
if (bytes < 1024) return bytes + " B";
|
||||||
|
int exp = (int) (Math.log(bytes) / Math.log(1024));
|
||||||
|
String pre = "KMGTPE".charAt(exp - 1) + "";
|
||||||
|
return String.format("%.1f %sB", bytes / Math.pow(1024, exp), pre);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifie les observateurs de l'initialisation du stockage.
|
||||||
|
*/
|
||||||
|
private void notifyStorageInitialized() {
|
||||||
|
StorageEvent event = new StorageEvent(
|
||||||
|
"STORAGE_INITIALIZED",
|
||||||
|
Map.of(
|
||||||
|
"basePath", appConfig.getStorageBasePath(),
|
||||||
|
"environment", appConfig.getEnvironment()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
storageEvent.fire(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide l'accès au répertoire.
|
||||||
|
*/
|
||||||
|
private void validateDirectoryAccess(Path path) {
|
||||||
|
if (!Files.isDirectory(path)) {
|
||||||
|
throw new StorageConfigurationException(
|
||||||
|
"Le chemin n'est pas un répertoire : " + path);
|
||||||
|
}
|
||||||
|
if (!Files.isWritable(path)) {
|
||||||
|
throw new StorageConfigurationException(
|
||||||
|
"Le répertoire n'est pas accessible en écriture : " + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,374 @@
|
|||||||
package dev.lions.controllers;
|
package dev.lions.controllers;
|
||||||
|
|
||||||
import jakarta.enterprise.context.RequestScoped;
|
import static java.time.LocalDateTime.ofInstant;
|
||||||
import jakarta.inject.Named;
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.enterprise.context.RequestScoped;
|
||||||
|
import jakarta.enterprise.event.Event;
|
||||||
|
import jakarta.faces.application.FacesMessage;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Named;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import dev.lions.models.*;
|
||||||
|
import dev.lions.services.*;
|
||||||
|
import dev.lions.events.AnalyticsEvent;
|
||||||
|
import dev.lions.utils.MessageUtils;
|
||||||
|
import dev.lions.exceptions.BusinessException;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrôleur principal de la page d'accueil.
|
||||||
|
* Gère l'ensemble des fonctionnalités et données affichées sur la landing page.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
@Named
|
@Named
|
||||||
@RequestScoped
|
@RequestScoped
|
||||||
public class HomeController implements Serializable {
|
public class HomeController implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
private static final String DEFAULT_FILTER = "all";
|
||||||
|
private static final Map<String, Integer> COMPANY_STATS = initializeCompanyStats();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ProjectService projectService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ContactService contactService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Event<AnalyticsEvent> analyticsEvent;
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
private String selectedFilter = DEFAULT_FILTER;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private List<Project> filteredProjects;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private List<ExpertiseArea> expertiseAreas;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private List<Service> services;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private List<ProcessStep> processSteps;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private String heroTitle = "Accélérez votre transformation digitale";
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private String heroDescription = "Des solutions innovantes pour booster votre croissance";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le contrôleur et charge les données nécessaires.
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
try {
|
||||||
|
log.info("Initialisation du contrôleur Home");
|
||||||
|
|
||||||
|
initializeData();
|
||||||
|
updateFilteredProjects();
|
||||||
|
fireInitializationEvent();
|
||||||
|
|
||||||
|
log.info("Contrôleur Home initialisé avec succès");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de l'initialisation du contrôleur", e);
|
||||||
|
MessageUtils.addErrorMessage("Erreur d'initialisation",
|
||||||
|
"Une erreur est survenue lors du chargement de la page");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour la liste des projets selon le filtre sélectionné.
|
||||||
|
*/
|
||||||
|
public void updateFilteredProjects() {
|
||||||
|
log.debug("Mise à jour des projets avec le filtre: {}", selectedFilter);
|
||||||
|
|
||||||
|
try {
|
||||||
|
filteredProjects = projectService.getFilteredProjects(selectedFilter);
|
||||||
|
fireFilterUpdateEvent();
|
||||||
|
} catch (BusinessException be) {
|
||||||
|
log.warn("Erreur lors du filtrage des projets", be);
|
||||||
|
MessageUtils.addWarningMessage("Attention", be.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur critique lors du filtrage", e);
|
||||||
|
MessageUtils.addErrorMessage("Erreur",
|
||||||
|
"Impossible de charger les projets");
|
||||||
|
filteredProjects = Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traite la soumission du formulaire de contact.
|
||||||
|
*
|
||||||
|
* @param form Le formulaire de contact soumis par l'utilisateur
|
||||||
|
*/
|
||||||
|
public void submitContactForm(@Valid @NotNull ContactForm form) {
|
||||||
|
log.info("Traitement du formulaire de contact");
|
||||||
|
|
||||||
|
try {
|
||||||
|
Contact contact = contactService.processContactForm(form);
|
||||||
|
fireContactSubmissionEvent(contact);
|
||||||
|
|
||||||
|
MessageUtils.addSuccessMessage("Message envoyé",
|
||||||
|
"Votre message a été envoyé avec succès");
|
||||||
|
|
||||||
|
} catch (BusinessException be) {
|
||||||
|
log.warn("Erreur de validation du formulaire", be);
|
||||||
|
MessageUtils.addWarningMessage("Validation", be.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors du traitement du formulaire", e);
|
||||||
|
MessageUtils.addErrorMessage("Erreur",
|
||||||
|
"Impossible de traiter votre demande");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les données statiques du contrôleur.
|
||||||
|
*/
|
||||||
|
private void initializeData() {
|
||||||
|
expertiseAreas = initializeExpertiseAreas();
|
||||||
|
services = initializeServices();
|
||||||
|
processSteps = initializeProcessSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise la liste des domaines d'expertise.
|
||||||
|
*
|
||||||
|
* @return La liste des domaines d'expertise
|
||||||
|
*/
|
||||||
|
private List<ExpertiseArea> initializeExpertiseAreas() {
|
||||||
|
log.debug("Initialisation des domaines d'expertise");
|
||||||
|
|
||||||
|
return List.of(
|
||||||
|
createExpertiseArea(
|
||||||
|
"Développement Sur Mesure",
|
||||||
|
"fa-code",
|
||||||
|
"Solutions logicielles adaptées à vos besoins",
|
||||||
|
List.of(
|
||||||
|
"Applications web modernes",
|
||||||
|
"Logiciels métier optimisés",
|
||||||
|
"APIs performantes"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
createExpertiseArea(
|
||||||
|
"Intégration de Solutions",
|
||||||
|
"fa-puzzle-piece",
|
||||||
|
"Déploiement et personnalisation",
|
||||||
|
List.of(
|
||||||
|
"Intégration CRM & ERP",
|
||||||
|
"Plateformes e-commerce",
|
||||||
|
"Solutions collaboratives"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un nouveau domaine d'expertise.
|
||||||
|
*
|
||||||
|
* @param title Le titre du domaine
|
||||||
|
* @param icon L'icône représentant le domaine
|
||||||
|
* @param description La description du domaine
|
||||||
|
* @param features Les fonctionnalités et avantages du domaine
|
||||||
|
* @return Le nouveau domaine d'expertise créé
|
||||||
|
*/
|
||||||
|
private ExpertiseArea createExpertiseArea(String title, String icon,
|
||||||
|
String description, List<String> features) {
|
||||||
|
return ExpertiseArea.builder()
|
||||||
|
.title(title)
|
||||||
|
.icon(icon)
|
||||||
|
.description(description)
|
||||||
|
.features(features)
|
||||||
|
.priority(1)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise la liste des services proposés.
|
||||||
|
*
|
||||||
|
* @return La liste des services
|
||||||
|
*/
|
||||||
|
private List<Service> initializeServices() {
|
||||||
|
log.debug("Initialisation des services");
|
||||||
|
|
||||||
|
return List.of(
|
||||||
|
createService(
|
||||||
|
"consulting",
|
||||||
|
"Conseil & Stratégie",
|
||||||
|
"fa-chart-line",
|
||||||
|
"Accompagnement stratégique",
|
||||||
|
List.of(
|
||||||
|
"Audit technique",
|
||||||
|
"Feuille de route digitale",
|
||||||
|
"Optimisation des processus"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
createService(
|
||||||
|
"development",
|
||||||
|
"Développement",
|
||||||
|
"fa-laptop-code",
|
||||||
|
"Création de solutions techniques",
|
||||||
|
List.of(
|
||||||
|
"Développement agile",
|
||||||
|
"Intégration continue",
|
||||||
|
"Support technique"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un nouveau service.
|
||||||
|
*
|
||||||
|
* @param id L'identifiant unique du service
|
||||||
|
* @param title Le titre du service
|
||||||
|
* @param icon L'icône représentant le service
|
||||||
|
* @param description La description du service
|
||||||
|
* @param benefits Les avantages et bénéfices du service
|
||||||
|
* @return Le nouveau service créé
|
||||||
|
*/
|
||||||
|
private Service createService(String id, String title, String icon,
|
||||||
|
String description, List<String> benefits) {
|
||||||
|
return Service.builder()
|
||||||
|
.id(id)
|
||||||
|
.title(title)
|
||||||
|
.icon(icon)
|
||||||
|
.description(description)
|
||||||
|
.benefits(benefits)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise la liste des étapes du processus.
|
||||||
|
*
|
||||||
|
* @return La liste des étapes du processus
|
||||||
|
*/
|
||||||
|
private List<ProcessStep> initializeProcessSteps() {
|
||||||
|
log.debug("Initialisation des étapes du processus");
|
||||||
|
|
||||||
|
return List.of(
|
||||||
|
createProcessStep(1, "Discovery", "Analyse des besoins", "2-3 semaines"),
|
||||||
|
createProcessStep(2, "Conception", "Élaboration de la solution", "3-4 semaines"),
|
||||||
|
createProcessStep(3, "Développement", "Création de la solution", "8-12 semaines"),
|
||||||
|
createProcessStep(4, "Déploiement", "Mise en production", "2-3 semaines")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle étape du processus.
|
||||||
|
*
|
||||||
|
* @param number Le numéro de l'étape
|
||||||
|
* @param title Le titre de l'étape
|
||||||
|
* @param description La description de l'étape
|
||||||
|
* @param duration La durée estimée de l'étape
|
||||||
|
* @return La nouvelle étape créée
|
||||||
|
*/
|
||||||
|
private ProcessStep createProcessStep(int number, String title,
|
||||||
|
String description, String duration) {
|
||||||
|
return ProcessStep.builder()
|
||||||
|
.number(number)
|
||||||
|
.title(title)
|
||||||
|
.description(description)
|
||||||
|
.duration(duration)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise la map des statistiques de l'entreprise.
|
||||||
|
*
|
||||||
|
* @return La map contenant les statistiques clés
|
||||||
|
*/
|
||||||
|
private static Map<String, Integer> initializeCompanyStats() {
|
||||||
|
Map<String, Integer> stats = new ConcurrentHashMap<>();
|
||||||
|
stats.put("Projets Réalisés", 150);
|
||||||
|
stats.put("Clients Satisfaits", 80);
|
||||||
|
stats.put("Experts Techniques", 25);
|
||||||
|
return Collections.unmodifiableMap(stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déclenche l'événement d'initialisation de la page d'accueil.
|
||||||
|
*/
|
||||||
|
private void fireInitializationEvent() {
|
||||||
|
analyticsEvent.fire(AnalyticsEvent.builder()
|
||||||
|
.eventType("HOME_INITIALIZED")
|
||||||
|
.timestamp(ofInstant(Instant.ofEpochMilli(Long.MAX_VALUE),
|
||||||
|
ZoneId.systemDefault()))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déclenche l'événement de mise à jour du filtre des projets.
|
||||||
|
*/
|
||||||
|
private void fireFilterUpdateEvent() {
|
||||||
|
analyticsEvent.fire(AnalyticsEvent.builder()
|
||||||
|
.eventType("PROJECTS_FILTERED")
|
||||||
|
.properties(Map.of("filter", selectedFilter))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déclenche l'événement de soumission du formulaire de contact.
|
||||||
|
*
|
||||||
|
* @param contact Le contact créé suite à la soumission du formulaire
|
||||||
|
*/
|
||||||
|
private void fireContactSubmissionEvent(Contact contact) {
|
||||||
|
analyticsEvent.fire(AnalyticsEvent.builder()
|
||||||
|
.eventType("CONTACT_SUBMITTED")
|
||||||
|
.contactId(contact.getId().toString())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le message d'accueil de la page d'accueil.
|
||||||
|
*
|
||||||
|
* @return Le message d'accueil
|
||||||
|
*/
|
||||||
public String getWelcomeMessage() {
|
public String getWelcomeMessage() {
|
||||||
System.out.println("HomeController.getWelcomeMessage() called");
|
return "Bienvenue chez Lions Dev";
|
||||||
return "Welcome to Lions Dev";
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le sous-titre affiché dans la section héros.
|
||||||
|
*
|
||||||
|
* @return Le sous-titre de la section héros
|
||||||
|
*/
|
||||||
|
public String getHeroSubtitle() {
|
||||||
|
return "Solutions digitales sur mesure";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne la liste des filtres disponibles pour les projets.
|
||||||
|
*
|
||||||
|
* @return La liste des filtres de projets
|
||||||
|
*/
|
||||||
|
public List<String> getProjectFilters() {
|
||||||
|
return List.of("Tous", "Web", "Mobile", "Cloud", "IA");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne les statistiques clés de l'entreprise.
|
||||||
|
*
|
||||||
|
* @return La map contenant les statistiques de l'entreprise
|
||||||
|
*/
|
||||||
|
public Map<String, Integer> getCompanyStats() {
|
||||||
|
return COMPANY_STATS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,164 @@
|
|||||||
package dev.lions.controllers;
|
package dev.lions.controllers;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.enterprise.context.RequestScoped;
|
import jakarta.enterprise.context.RequestScoped;
|
||||||
|
import jakarta.enterprise.event.Event;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Named;
|
import jakarta.inject.Named;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import dev.lions.config.ApplicationConfig;
|
||||||
|
import dev.lions.events.AnalyticsEvent;
|
||||||
|
import dev.lions.services.ProjectService;
|
||||||
|
import dev.lions.exceptions.InitializationException;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrôleur de la page d'index.
|
||||||
|
* Gère l'affichage et les interactions de la page d'accueil principale
|
||||||
|
* en assurant le chargement des messages localisés et des données nécessaires.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
@Named
|
@Named
|
||||||
@RequestScoped
|
@RequestScoped
|
||||||
public class IndexController {
|
public class IndexController {
|
||||||
|
|
||||||
private String message = "Welcome to Lions Dev!";
|
private static final String BUNDLE_NAME = "messages";
|
||||||
|
private static final String DEFAULT_LOCALE = "fr";
|
||||||
|
|
||||||
public String getMessage() {
|
@Inject
|
||||||
return message;
|
ApplicationConfig applicationConfig;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ProjectService projectService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Event<AnalyticsEvent> analyticsEvent;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private String welcomeMessage;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private String applicationName;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private String environment;
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
private ResourceBundle messageBundle;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
log.info("Initialisation du contrôleur Index");
|
||||||
|
try {
|
||||||
|
initializeMessageBundle();
|
||||||
|
initializeApplicationSettings();
|
||||||
|
notifyPageView();
|
||||||
|
log.info("Contrôleur Index initialisé avec succès");
|
||||||
|
} catch (Exception e) {
|
||||||
|
String errorMessage = "Erreur lors de l'initialisation du contrôleur Index";
|
||||||
|
log.error(errorMessage, e);
|
||||||
|
throw new InitializationException(errorMessage, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMessage(String message) {
|
/**
|
||||||
this.message = message;
|
* Récupère un message localisé avec une clé donnée.
|
||||||
|
* La méthode est maintenant protégée pour permettre l'interception par CDI
|
||||||
|
*/
|
||||||
|
protected String getMessage(@NotNull String key) {
|
||||||
|
try {
|
||||||
|
return messageBundle.getString(key);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Message non trouvé pour la clé: {}", key);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'environnement est en mode développement.
|
||||||
|
*/
|
||||||
|
public boolean isDevelopmentMode() {
|
||||||
|
return applicationConfig.isDevelopment();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le nombre total de projets.
|
||||||
|
*/
|
||||||
|
public long getProjectCount() {
|
||||||
|
try {
|
||||||
|
return projectService.getProjectCount();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Erreur lors de la récupération du nombre de projets", e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne un message de statut pour le mode développement.
|
||||||
|
*/
|
||||||
|
public String getStatusMessage() {
|
||||||
|
if (isDevelopmentMode()) {
|
||||||
|
return String.format("Mode développement - Environnement: %s", environment);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recharge les messages et paramètres du contrôleur.
|
||||||
|
*/
|
||||||
|
public void refresh() {
|
||||||
|
log.info("Rafraîchissement des données du contrôleur Index");
|
||||||
|
initializeMessageBundle();
|
||||||
|
initializeApplicationSettings();
|
||||||
|
notifyPageView();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le bundle de messages localisés.
|
||||||
|
*/
|
||||||
|
protected void initializeMessageBundle() {
|
||||||
|
log.debug("Chargement des messages localisés");
|
||||||
|
Locale locale = new Locale(DEFAULT_LOCALE);
|
||||||
|
messageBundle = ResourceBundle.getBundle(BUNDLE_NAME, locale);
|
||||||
|
|
||||||
|
welcomeMessage = getMessage("welcome.message");
|
||||||
|
message = String.format("%s - %s",
|
||||||
|
getMessage("index.greeting"),
|
||||||
|
applicationConfig.getApplicationName());
|
||||||
|
|
||||||
|
log.debug("Messages localisés chargés avec succès");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les paramètres de l'application.
|
||||||
|
*/
|
||||||
|
protected void initializeApplicationSettings() {
|
||||||
|
log.debug("Chargement des paramètres de l'application");
|
||||||
|
applicationName = applicationConfig.getApplicationName();
|
||||||
|
environment = applicationConfig.getEnvironment();
|
||||||
|
log.debug("Paramètres de l'application chargés : env={}", environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifie le système d'une vue de page.
|
||||||
|
*/
|
||||||
|
protected void notifyPageView() {
|
||||||
|
analyticsEvent.fire(AnalyticsEvent.builder()
|
||||||
|
.eventType("PAGE_VIEW")
|
||||||
|
.properties(Map.of(
|
||||||
|
"page", "index",
|
||||||
|
"environment", environment,
|
||||||
|
"timestamp", System.currentTimeMillis()
|
||||||
|
))
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
208
src/main/java/dev/lions/controllers/NavigationController.java
Normal file
208
src/main/java/dev/lions/controllers/NavigationController.java
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
package dev.lions.controllers;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.SessionScoped;
|
||||||
|
import jakarta.enterprise.event.Event;
|
||||||
|
import jakarta.faces.context.FacesContext;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Named;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import dev.lions.events.NavigationEvent;
|
||||||
|
import dev.lions.services.ProjectService;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrôleur de navigation principal de l'application.
|
||||||
|
* Gère les transitions entre les pages et la persistance des paramètres
|
||||||
|
* de navigation de manière sécurisée et optimisée.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Named("navigationController")
|
||||||
|
@SessionScoped
|
||||||
|
public class NavigationController implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private static final String DEFAULT_OUTCOME = "/public/index.xhtml";
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ProjectService projectService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Event<NavigationEvent> navigationEvent;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private String currentPage;
|
||||||
|
|
||||||
|
private final Map<String, Object> navigationParams = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirige vers la page d'accueil avec rafraîchissement.
|
||||||
|
*
|
||||||
|
* @return Résultat de la navigation JSF
|
||||||
|
*/
|
||||||
|
public String goToHome() {
|
||||||
|
log.info("Navigation vers la page d'accueil");
|
||||||
|
notifyNavigation("home");
|
||||||
|
return DEFAULT_OUTCOME + "?faces-redirect=true";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirige vers la page des services.
|
||||||
|
*
|
||||||
|
* @return Résultat de la navigation JSF
|
||||||
|
*/
|
||||||
|
public String goToServices() {
|
||||||
|
log.info("Navigation vers la page des services");
|
||||||
|
notifyNavigation("services");
|
||||||
|
return "/private/services.xhtml?faces-redirect=true";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirige vers la page du portfolio.
|
||||||
|
*
|
||||||
|
* @return Résultat de la navigation JSF
|
||||||
|
*/
|
||||||
|
public String goToPortfolio() {
|
||||||
|
log.info("Navigation vers le portfolio");
|
||||||
|
notifyNavigation("portfolio");
|
||||||
|
return "/private/portfolio.xhtml?faces-redirect=true";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirige vers la page de détails d'un projet spécifique.
|
||||||
|
*
|
||||||
|
* @param projectId Identifiant du projet
|
||||||
|
* @return Résultat de la navigation JSF
|
||||||
|
*/
|
||||||
|
public String goToProjectDetails(@NotNull String projectId) {
|
||||||
|
log.info("Navigation vers les détails du projet: {}", projectId);
|
||||||
|
|
||||||
|
if (!projectService.existsById(projectId)) {
|
||||||
|
log.warn("Tentative d'accès à un projet inexistant: {}", projectId);
|
||||||
|
addErrorMessage();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyNavigation("project_details", Map.of("projectId", projectId));
|
||||||
|
return "/private/project-details.xhtml?faces-redirect=true&id=" + projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirige vers la page de détails d'un service spécifique.
|
||||||
|
*
|
||||||
|
* @param serviceId Identifiant du service
|
||||||
|
* @return Résultat de la navigation JSF
|
||||||
|
*/
|
||||||
|
public String goToServiceDetails(@NotNull String serviceId) {
|
||||||
|
log.info("Navigation vers les détails du service: {}", serviceId);
|
||||||
|
notifyNavigation("service_details", Map.of("serviceId", serviceId));
|
||||||
|
return "/private/service-details.xhtml?faces-redirect=true&id=" + serviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirige vers la page de contact.
|
||||||
|
*
|
||||||
|
* @return Résultat de la navigation JSF
|
||||||
|
*/
|
||||||
|
public String goToContact() {
|
||||||
|
log.info("Navigation vers la page de contact");
|
||||||
|
notifyNavigation("contact");
|
||||||
|
return "/private/contact.xhtml?faces-redirect=true";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sauvegarde un paramètre pour la navigation.
|
||||||
|
*
|
||||||
|
* @param key Clé du paramètre
|
||||||
|
* @param value Valeur du paramètre
|
||||||
|
*/
|
||||||
|
public void setNavigationParameter(@NotNull String key, Object value) {
|
||||||
|
navigationParams.put(key, value);
|
||||||
|
log.debug("Paramètre de navigation défini: {} = {}", key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un paramètre de navigation.
|
||||||
|
*
|
||||||
|
* @param key Clé du paramètre
|
||||||
|
* @return Valeur du paramètre ou null si non trouvé
|
||||||
|
*/
|
||||||
|
public Object getNavigationParameter(String key) {
|
||||||
|
return navigationParams.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si la page actuelle correspond à un chemin donné.
|
||||||
|
*
|
||||||
|
* @param viewId Identifiant de la vue
|
||||||
|
* @return true si la page courante correspond
|
||||||
|
*/
|
||||||
|
public boolean isCurrentPage(String viewId) {
|
||||||
|
String currentViewId = FacesContext.getCurrentInstance()
|
||||||
|
.getViewRoot()
|
||||||
|
.getViewId();
|
||||||
|
return currentViewId.equals(viewId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie les paramètres de navigation.
|
||||||
|
*/
|
||||||
|
public void clearNavigationParameters() {
|
||||||
|
navigationParams.clear();
|
||||||
|
log.debug("Paramètres de navigation nettoyés");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise un nouveau contexte de navigation.
|
||||||
|
*/
|
||||||
|
public void initializeNavigation() {
|
||||||
|
currentPage = Optional.ofNullable(FacesContext.getCurrentInstance())
|
||||||
|
.map(context -> context.getViewRoot().getViewId())
|
||||||
|
.orElse(DEFAULT_OUTCOME);
|
||||||
|
|
||||||
|
log.info("Navigation initialisée sur la page: {}", currentPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifie le système d'un événement de navigation.
|
||||||
|
*/
|
||||||
|
private void notifyNavigation(String destination) {
|
||||||
|
notifyNavigation(destination, new HashMap<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifie le système d'un événement de navigation avec paramètres.
|
||||||
|
*/
|
||||||
|
private void notifyNavigation(String destination, Map<String, Object> params) {
|
||||||
|
NavigationEvent event = NavigationEvent.builder()
|
||||||
|
.source(currentPage)
|
||||||
|
.destination(destination)
|
||||||
|
.parameters(params)
|
||||||
|
.timestamp(System.currentTimeMillis())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
navigationEvent.fire(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute un message d'erreur dans le contexte Faces.
|
||||||
|
*/
|
||||||
|
private void addErrorMessage() {
|
||||||
|
FacesContext.getCurrentInstance()
|
||||||
|
.addMessage(null,
|
||||||
|
new jakarta.faces.application.FacesMessage(
|
||||||
|
jakarta.faces.application.FacesMessage.SEVERITY_ERROR,
|
||||||
|
"Erreur", "Projet introuvable"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/main/java/dev/lions/dtos/NotificationDTO.java
Normal file
52
src/main/java/dev/lions/dtos/NotificationDTO.java
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package dev.lions.dtos;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import dev.lions.models.Notification;
|
||||||
|
import dev.lions.models.NotificationStatus;
|
||||||
|
import dev.lions.models.NotificationType;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@Slf4j
|
||||||
|
public class NotificationDTO {
|
||||||
|
private Long id;
|
||||||
|
private String title;
|
||||||
|
private String message;
|
||||||
|
private NotificationType type;
|
||||||
|
private NotificationStatus status;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
private String actionUrl;
|
||||||
|
private Long targetUserId;
|
||||||
|
|
||||||
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
public static NotificationDTO from(Notification notification) {
|
||||||
|
return NotificationDTO.builder()
|
||||||
|
.id(notification.getId())
|
||||||
|
.title(notification.getTitle())
|
||||||
|
.message(notification.getMessage())
|
||||||
|
.type(notification.getType())
|
||||||
|
.status(notification.getStatus())
|
||||||
|
.timestamp(notification.getTimestamp())
|
||||||
|
.actionUrl(notification.getActionUrl())
|
||||||
|
.targetUserId(notification.getTargetUserId())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toJson() {
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(this);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error converting notification to JSON", e);
|
||||||
|
return String.format("{\"error\":\"Failed to serialize notification %d\"}", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
226
src/main/java/dev/lions/events/AnalyticsEvent.java
Normal file
226
src/main/java/dev/lions/events/AnalyticsEvent.java
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
package dev.lions.events;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.Convert;
|
||||||
|
import jakarta.persistence.Temporal;
|
||||||
|
import jakarta.persistence.TemporalType;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import dev.lions.utils.JsonConverter;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entité représentant un événement analytique dans le système.
|
||||||
|
* Cette classe permet de tracer et d'analyser les différentes actions et interactions
|
||||||
|
* des utilisateurs avec l'application.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 1.1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "analytics_events")
|
||||||
|
@Builder(toBuilder = true)
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Accessors(chain = true)
|
||||||
|
public class AnalyticsEvent implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifiant unique de l'événement
|
||||||
|
*/
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type d'événement analytique (ex: PAGE_VIEW, USER_ACTION, etc.)
|
||||||
|
*/
|
||||||
|
@NotNull(message = "Le type d'événement est obligatoire")
|
||||||
|
@Column(name = "event_type", nullable = false)
|
||||||
|
private String eventType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifiant de l'utilisateur associé à l'événement
|
||||||
|
*/
|
||||||
|
@Column(name = "user_id")
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifiant du contact associé à l'événement
|
||||||
|
*/
|
||||||
|
@Column(name = "contact_id")
|
||||||
|
private String contactId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source de l'événement (ex: WEB, MOBILE, API)
|
||||||
|
*/
|
||||||
|
@Column(name = "source")
|
||||||
|
private String source;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Propriétés additionnelles de l'événement stockées au format JSON
|
||||||
|
*/
|
||||||
|
@Convert(converter = JsonConverter.class)
|
||||||
|
@Column(name = "properties", columnDefinition = "jsonb")
|
||||||
|
@Builder.Default
|
||||||
|
private Map<String, Object> properties = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date et heure de l'événement
|
||||||
|
*/
|
||||||
|
@Temporal(TemporalType.TIMESTAMP)
|
||||||
|
@Column(name = "timestamp", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private LocalDateTime timestamp = LocalDateTime.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environnement dans lequel l'événement s'est produit
|
||||||
|
*/
|
||||||
|
@Column(name = "environment", nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private String environment = System.getProperty("app.environment", "production");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type d'événements analytiques supportés
|
||||||
|
*/
|
||||||
|
public enum EventType {
|
||||||
|
PAGE_VIEW,
|
||||||
|
USER_ACTION,
|
||||||
|
SYSTEM_EVENT,
|
||||||
|
ERROR,
|
||||||
|
PERFORMANCE,
|
||||||
|
SECURITY
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une copie de l'événement avec des propriétés enrichies
|
||||||
|
*
|
||||||
|
* @param additionalProps Propriétés supplémentaires à ajouter
|
||||||
|
* @return Nouvelle instance d'AnalyticsEvent avec les propriétés enrichies
|
||||||
|
*/
|
||||||
|
public AnalyticsEvent withAdditionalProperties(Map<String, Object> additionalProps) {
|
||||||
|
if (additionalProps == null || additionalProps.isEmpty()) {
|
||||||
|
log.debug("Aucune propriété additionnelle à ajouter");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Ajout de {} propriétés additionnelles à l'événement", additionalProps.size());
|
||||||
|
|
||||||
|
Map<String, Object> newProps = new HashMap<>(this.properties);
|
||||||
|
newProps.putAll(additionalProps);
|
||||||
|
|
||||||
|
return this.toBuilder()
|
||||||
|
.properties(newProps)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une propriété unique à l'événement
|
||||||
|
*
|
||||||
|
* @param key Clé de la propriété
|
||||||
|
* @param value Valeur de la propriété
|
||||||
|
* @return L'instance actuelle pour chaînage
|
||||||
|
*/
|
||||||
|
public AnalyticsEvent addProperty(String key, Object value) {
|
||||||
|
if (key == null || key.trim().isEmpty()) {
|
||||||
|
log.warn("Tentative d'ajout d'une propriété avec une clé nulle ou vide");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Ajout de la propriété '{}' à l'événement", key);
|
||||||
|
this.properties.put(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enrichit l'événement avec des métadonnées standard
|
||||||
|
*
|
||||||
|
* @return L'instance actuelle pour chaînage
|
||||||
|
*/
|
||||||
|
public AnalyticsEvent enrichWithMetadata() {
|
||||||
|
log.debug("Enrichissement de l'événement {} avec les métadonnées standard", this.id);
|
||||||
|
|
||||||
|
this.addProperty("timestamp_ms", System.currentTimeMillis())
|
||||||
|
.addProperty("java_version", System.getProperty("java.version"))
|
||||||
|
.addProperty("os_name", System.getProperty("os.name"))
|
||||||
|
.addProperty("app_version", System.getProperty("app.version"))
|
||||||
|
.addProperty("node_id", System.getProperty("node.id"))
|
||||||
|
.addProperty("thread_name", Thread.currentThread().getName());
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'événement est valide pour le traitement
|
||||||
|
*/
|
||||||
|
public boolean isValid() {
|
||||||
|
boolean isValid = this.eventType != null &&
|
||||||
|
!this.eventType.trim().isEmpty() &&
|
||||||
|
this.timestamp != null;
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
log.warn("Événement invalide détecté: type={}, timestamp={}",
|
||||||
|
this.eventType, this.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marque l'événement comme ayant été traité
|
||||||
|
*
|
||||||
|
* @param processingDetails Détails du traitement
|
||||||
|
* @return L'instance actuelle pour chaînage
|
||||||
|
*/
|
||||||
|
public AnalyticsEvent markAsProcessed(Map<String, Object> processingDetails) {
|
||||||
|
log.debug("Marquage de l'événement {} comme traité", this.id);
|
||||||
|
|
||||||
|
return this.addProperty("processed_at", LocalDateTime.now().toString())
|
||||||
|
.addProperty("processing_details", processingDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suit la soumission d'un contact
|
||||||
|
*/
|
||||||
|
public void trackContactSubmission() {
|
||||||
|
log.info("Suivi de la soumission pour l'événement: {}", this);
|
||||||
|
this.addProperty("submission_tracked", true)
|
||||||
|
.addProperty("submission_time", LocalDateTime.now().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format(
|
||||||
|
"AnalyticsEvent[id=%d, type=%s, userId=%s, timestamp=%s, env=%s]",
|
||||||
|
id, eventType, userId, timestamp, environment
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une copie de l'événement
|
||||||
|
*
|
||||||
|
* @return Nouvelle instance avec les mêmes données
|
||||||
|
*/
|
||||||
|
public AnalyticsEvent copy() {
|
||||||
|
return this.toBuilder().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/main/java/dev/lions/events/AnalyticsEventPublisher.java
Normal file
31
src/main/java/dev/lions/events/AnalyticsEventPublisher.java
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package dev.lions.events;
|
||||||
|
|
||||||
|
import dev.lions.events.AnalyticsEvent;
|
||||||
|
import dev.lions.exceptions.EventPublicationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface définissant les opérations de publication des événements analytiques.
|
||||||
|
* Cette interface fournit les méthodes nécessaires pour publier des événements
|
||||||
|
* de manière individuelle ou par lot.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 1.0
|
||||||
|
*/
|
||||||
|
public interface AnalyticsEventPublisher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publie un événement analytique unique.
|
||||||
|
*
|
||||||
|
* @param event L'événement à publier
|
||||||
|
* @throws EventPublicationException Si la publication échoue
|
||||||
|
*/
|
||||||
|
void publish(AnalyticsEvent event) throws EventPublicationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publie un lot d'événements analytiques.
|
||||||
|
*
|
||||||
|
* @param events Collection d'événements à publier
|
||||||
|
* @throws EventPublicationException Si la publication d'un des événements échoue
|
||||||
|
*/
|
||||||
|
void publishBatch(Iterable<AnalyticsEvent> events) throws EventPublicationException;
|
||||||
|
}
|
||||||
43
src/main/java/dev/lions/events/ConfigurationEvent.java
Normal file
43
src/main/java/dev/lions/events/ConfigurationEvent.java
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package dev.lions.events;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Événement déclenché lors de l'initialisation du service de configuration.
|
||||||
|
* Cet événement permet de notifier les observateurs des changements de
|
||||||
|
* configuration de l'application.
|
||||||
|
*/
|
||||||
|
public class ConfigurationEvent {
|
||||||
|
|
||||||
|
private final String type;
|
||||||
|
private final Map<String, Object> data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance de ConfigurationEvent.
|
||||||
|
*
|
||||||
|
* @param type Type de l'événement de configuration
|
||||||
|
* @param data Données associées à l'événement
|
||||||
|
*/
|
||||||
|
public ConfigurationEvent(String type, Map<String, Object> data) {
|
||||||
|
this.type = type;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le type de l'événement de configuration.
|
||||||
|
*
|
||||||
|
* @return Type de l'événement
|
||||||
|
*/
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les données associées à l'événement de configuration.
|
||||||
|
*
|
||||||
|
* @return Données de l'événement
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/main/java/dev/lions/events/ContactEventHandler.java
Normal file
60
src/main/java/dev/lions/events/ContactEventHandler.java
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package dev.lions.events;
|
||||||
|
|
||||||
|
import dev.lions.exceptions.EventProcessingException;
|
||||||
|
import dev.lions.models.Contact;
|
||||||
|
import dev.lions.services.NotificationService;
|
||||||
|
import dev.lions.models.NotificationType;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.event.Observes;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestionnaire des événements liés aux contacts. Traite les soumissions de formulaires de contact
|
||||||
|
* et déclenche les actions appropriées.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ContactEventHandler {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private NotificationService notificationService;
|
||||||
|
|
||||||
|
public void onContactSubmission(@Observes ContactSubmissionEvent event) {
|
||||||
|
try {
|
||||||
|
Contact contact = event.getContact();
|
||||||
|
processAnalytics(contact);
|
||||||
|
sendNotifications(contact);
|
||||||
|
|
||||||
|
log.info("Contact submission event processed successfully for contact ID: {}",
|
||||||
|
contact.getId());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error processing contact submission event", e);
|
||||||
|
throw new EventProcessingException("Failed to process contact submission", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processAnalytics(Contact contact) {
|
||||||
|
Map<String, Object> properties = new HashMap<>();
|
||||||
|
properties.put("subject", contact.getSubject());
|
||||||
|
properties.put("hasCompany", contact.getCompany() != null);
|
||||||
|
properties.put("submissionTime", contact.getSubmitDate());
|
||||||
|
|
||||||
|
AnalyticsEvent analyticsEvent =
|
||||||
|
AnalyticsEvent.builder().eventType("CONTACT_SUBMISSION").contactId(
|
||||||
|
String.valueOf(contact.getId()))
|
||||||
|
.properties(properties).build();
|
||||||
|
|
||||||
|
analyticsEvent.trackContactSubmission();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendNotifications(Contact contact) {
|
||||||
|
notificationService.sendInternalNotification(NotificationType.NEW_CONTACT,
|
||||||
|
String.format("Nouveau message de %s : %s",
|
||||||
|
contact.getName(),
|
||||||
|
contact.getSubject()));
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/dev/lions/events/ContactSubmissionEvent.java
Normal file
23
src/main/java/dev/lions/events/ContactSubmissionEvent.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package dev.lions.events;
|
||||||
|
|
||||||
|
import dev.lions.models.Contact;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Événement émis lors de la soumission d'un nouveau formulaire de contact.
|
||||||
|
* Cet événement permet de découpler le traitement des contacts de leur soumission.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ContactSubmissionEvent {
|
||||||
|
private final Contact contact;
|
||||||
|
private final LocalDateTime timestamp;
|
||||||
|
|
||||||
|
public ContactSubmissionEvent(Contact contact) {
|
||||||
|
this.contact = contact;
|
||||||
|
this.timestamp = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
16
src/main/java/dev/lions/events/FileUploadEvent.java
Normal file
16
src/main/java/dev/lions/events/FileUploadEvent.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package dev.lions.events;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Événement de notification lors du téléchargement d'un fichier.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class FileUploadEvent {
|
||||||
|
private String fileId;
|
||||||
|
private String fileName;
|
||||||
|
private long size;
|
||||||
|
private long timestamp;
|
||||||
|
}
|
||||||
19
src/main/java/dev/lions/events/NavigationEvent.java
Normal file
19
src/main/java/dev/lions/events/NavigationEvent.java
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package dev.lions.events;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Événement pour la navigation.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class NavigationEvent {
|
||||||
|
private String action;
|
||||||
|
private String source;
|
||||||
|
private String destination;
|
||||||
|
private Map<String, Object> parameters; // Nouveau champ pour les paramètres
|
||||||
|
private long timestamp; // Nouveau champ pour le timestamp
|
||||||
|
}
|
||||||
40
src/main/java/dev/lions/events/ProjectEventHandler.java
Normal file
40
src/main/java/dev/lions/events/ProjectEventHandler.java
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package dev.lions.events;
|
||||||
|
|
||||||
|
import dev.lions.exceptions.EventProcessingException;
|
||||||
|
import dev.lions.utils.CacheService;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.event.Observes;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ProjectEventHandler {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private CacheService cacheService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private SearchIndexService searchIndexService;
|
||||||
|
|
||||||
|
public void onProjectUpdate(@Observes ProjectUpdateEvent event) {
|
||||||
|
try {
|
||||||
|
// Invalidation du cache
|
||||||
|
cacheService.invalidateProjectCache(event.getProjectId());
|
||||||
|
|
||||||
|
// Mise à jour de l'index de recherche
|
||||||
|
if ("CREATE".equals(event.getAction()) || "UPDATE".equals(event.getAction())) {
|
||||||
|
searchIndexService.indexProject(event.getProjectId());
|
||||||
|
} else if ("DELETE".equals(event.getAction())) {
|
||||||
|
searchIndexService.removeFromIndex(event.getProjectId());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Project event processed successfully. Action: {}, Project ID: {}",
|
||||||
|
event.getAction(), event.getProjectId());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error processing project event", e);
|
||||||
|
throw new EventProcessingException("Failed to process project event", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/main/java/dev/lions/events/ProjectUpdateEvent.java
Normal file
35
src/main/java/dev/lions/events/ProjectUpdateEvent.java
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package dev.lions.events;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Événement émis lors de la modification ou création d'un projet. Permet de gérer les mises à jour
|
||||||
|
* asynchrones (cache, indexation, etc.).
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ProjectUpdateEvent {
|
||||||
|
private final String projectId;
|
||||||
|
private final String action; // CREATE, UPDATE, DELETE
|
||||||
|
private final LocalDateTime timestamp;
|
||||||
|
|
||||||
|
public ProjectUpdateEvent(String projectId, String action) {
|
||||||
|
this.projectId = projectId;
|
||||||
|
this.action = action;
|
||||||
|
this.timestamp = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCreate() {
|
||||||
|
return "CREATE".equals(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUpdate() {
|
||||||
|
return "UPDATE".equals(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDelete() {
|
||||||
|
return "DELETE".equals(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package dev.lions.events;
|
||||||
|
|
||||||
|
import dev.lions.exceptions.EventPublicationException;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.event.Event;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implémentation du publisher d'événements analytiques utilisant le système
|
||||||
|
* d'événements CDI de Quarkus pour le traitement asynchrone.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 1.1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class QuarkusAnalyticsEventPublisher implements AnalyticsEventPublisher {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Event<AnalyticsEvent> eventBus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publie un événement analytique de manière asynchrone.
|
||||||
|
*
|
||||||
|
* @param event L'événement à publier
|
||||||
|
* @throws EventPublicationException Si la publication échoue
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void publish(AnalyticsEvent event) {
|
||||||
|
log.debug("Publication d'un événement analytique de type: {}", event.getEventType());
|
||||||
|
|
||||||
|
try {
|
||||||
|
eventBus.fireAsync(event)
|
||||||
|
.handle((success, error) -> {
|
||||||
|
if (error != null) {
|
||||||
|
log.error("Erreur lors de la publication de l'événement analytique: {}",
|
||||||
|
error.getMessage(), error);
|
||||||
|
throw new EventPublicationException(
|
||||||
|
"Échec de la publication de l'événement analytique", error);
|
||||||
|
} else {
|
||||||
|
log.debug("Événement analytique publié avec succès: {}",
|
||||||
|
event.getEventType());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur inattendue lors de la publication de l'événement", e);
|
||||||
|
throw new EventPublicationException(
|
||||||
|
"Échec de la publication de l'événement analytique", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publie un lot d'événements analytiques.
|
||||||
|
*
|
||||||
|
* @param events Collection d'événements à publier
|
||||||
|
* @throws EventPublicationException Si la publication d'un des événements échoue
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void publishBatch(Iterable<AnalyticsEvent> events) {
|
||||||
|
log.debug("Début de la publication du lot d'événements");
|
||||||
|
AtomicInteger count = new AtomicInteger(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
events.forEach(event -> {
|
||||||
|
publish(event);
|
||||||
|
count.incrementAndGet();
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("Lot de {} événements publié avec succès", count.get());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la publication du lot après {} événements", count.get(), e);
|
||||||
|
throw new EventPublicationException(
|
||||||
|
String.format("Échec de la publication du lot après %d événements", count.get()),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/dev/lions/events/SearchDocument.java
Normal file
25
src/main/java/dev/lions/events/SearchDocument.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package dev.lions.events;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Document de recherche pour l'indexation dans Elasticsearch.
|
||||||
|
* Cette classe représente la structure des documents indexés.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SearchDocument {
|
||||||
|
private String id;
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private List<String> tags;
|
||||||
|
private List<String> technologies;
|
||||||
|
private LocalDateTime completionDate;
|
||||||
|
}
|
||||||
37
src/main/java/dev/lions/events/SearchIndexService.java
Normal file
37
src/main/java/dev/lions/events/SearchIndexService.java
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package dev.lions.events;
|
||||||
|
|
||||||
|
import dev.lions.models.Project;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service simplifié pour gérer un index simulé sans Elasticsearch.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class SearchIndexService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode simulée pour indexer un projet.
|
||||||
|
*
|
||||||
|
* @param projectId Identifiant unique du projet à indexer
|
||||||
|
*/
|
||||||
|
public void indexProject(String projectId) {
|
||||||
|
log.info("Simulation d'indexation du projet: {}", projectId);
|
||||||
|
|
||||||
|
// Simulation de logique
|
||||||
|
log.debug("Projet {} ajouté à l'index simulé.", projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode simulée pour supprimer un projet de l'index.
|
||||||
|
*
|
||||||
|
* @param projectId Identifiant unique du projet à supprimer de l'index
|
||||||
|
*/
|
||||||
|
public void removeFromIndex(String projectId) {
|
||||||
|
log.info("Simulation de suppression du projet de l'index: {}", projectId);
|
||||||
|
|
||||||
|
// Simulation de logique
|
||||||
|
log.debug("Projet {} supprimé de l'index simulé.", projectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/main/java/dev/lions/events/StorageEvent.java
Normal file
42
src/main/java/dev/lions/events/StorageEvent.java
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package dev.lions.events;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Événement déclenché lors de l'initialisation du service de stockage. Cet événement permet de
|
||||||
|
* notifier les observateurs des changements de configuration du stockage de l'application.
|
||||||
|
*/
|
||||||
|
public class StorageEvent {
|
||||||
|
|
||||||
|
private final String type;
|
||||||
|
private final Map<String, Object> data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance de StorageEvent.
|
||||||
|
*
|
||||||
|
* @param type Type de l'événement de stockage
|
||||||
|
* @param data Données associées à l'événement
|
||||||
|
*/
|
||||||
|
public StorageEvent(String type, Map<String, Object> data) {
|
||||||
|
this.type = type;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le type de l'événement de stockage.
|
||||||
|
*
|
||||||
|
* @return Type de l'événement
|
||||||
|
*/
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les données associées à l'événement de stockage.
|
||||||
|
*
|
||||||
|
* @return Données de l'événement
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/main/java/dev/lions/exceptions/AnalyticsException.java
Normal file
17
src/main/java/dev/lions/exceptions/AnalyticsException.java
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception spécifique pour les erreurs liées au traitement des événements analytiques. Cette
|
||||||
|
* exception encapsule les erreurs qui surviennent lors de l'enregistrement, l'enrichissement ou la
|
||||||
|
* publication des événements d'analyse.
|
||||||
|
*/
|
||||||
|
public class AnalyticsException extends RuntimeException {
|
||||||
|
|
||||||
|
public AnalyticsException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnalyticsException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/main/java/dev/lions/exceptions/BusinessException.java
Normal file
12
src/main/java/dev/lions/exceptions/BusinessException.java
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
public class BusinessException extends RuntimeException {
|
||||||
|
|
||||||
|
public BusinessException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BusinessException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception levée lorsqu'une erreur de configuration se produit.
|
||||||
|
* Cette exception encapsule les erreurs liées à la configuration
|
||||||
|
* de l'application, telles que des paramètres invalides ou des
|
||||||
|
* ressources indisponibles.
|
||||||
|
*/
|
||||||
|
public class ConfigurationException extends RuntimeException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance de ConfigurationException avec un message.
|
||||||
|
*
|
||||||
|
* @param message Message décrivant l'erreur de configuration
|
||||||
|
*/
|
||||||
|
public ConfigurationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance de ConfigurationException avec un message et une cause.
|
||||||
|
*
|
||||||
|
* @param message Message décrivant l'erreur de configuration
|
||||||
|
* @param cause Cause à l'origine de l'exception
|
||||||
|
*/
|
||||||
|
public ConfigurationException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
src/main/java/dev/lions/exceptions/DataTableException.java
Normal file
103
src/main/java/dev/lions/exceptions/DataTableException.java
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception spécifique pour la gestion des erreurs liées aux tables de données.
|
||||||
|
* Cette exception encapsule les problèmes survenant lors de la manipulation,
|
||||||
|
* du tri ou du filtrage des données tabulaires.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 1.0
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public class DataTableException extends BusinessException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifiant de la table concernée par l'erreur
|
||||||
|
* -- GETTER --
|
||||||
|
* Récupère l'identifiant de la table concernée.
|
||||||
|
*
|
||||||
|
* @return Identifiant de la table ou null si non spécifié
|
||||||
|
|
||||||
|
*/
|
||||||
|
private final String tableId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type d'opération ayant échoué
|
||||||
|
* -- GETTER --
|
||||||
|
* Récupère l'opération ayant échoué.
|
||||||
|
*
|
||||||
|
* @return Type d'opération ou null si non spécifié
|
||||||
|
|
||||||
|
*/
|
||||||
|
private final DataTableOperation operation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance avec un message d'erreur.
|
||||||
|
*
|
||||||
|
* @param message Description détaillée de l'erreur
|
||||||
|
*/
|
||||||
|
public DataTableException(String message) {
|
||||||
|
this(message, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance avec un message et une cause.
|
||||||
|
*
|
||||||
|
* @param message Description détaillée de l'erreur
|
||||||
|
* @param cause Cause originale de l'erreur
|
||||||
|
*/
|
||||||
|
public DataTableException(String message, Throwable cause) {
|
||||||
|
this(message, cause, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance avec tous les détails de l'erreur.
|
||||||
|
*
|
||||||
|
* @param message Description détaillée de l'erreur
|
||||||
|
* @param cause Cause originale de l'erreur
|
||||||
|
* @param tableId Identifiant de la table concernée
|
||||||
|
* @param operation Opération ayant échoué
|
||||||
|
*/
|
||||||
|
public DataTableException(String message, Throwable cause, String tableId, DataTableOperation operation) {
|
||||||
|
super(message, cause);
|
||||||
|
this.tableId = tableId;
|
||||||
|
this.operation = operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types d'opérations pouvant échouer sur une table de données.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum DataTableOperation {
|
||||||
|
SORT("Tri"),
|
||||||
|
FILTER("Filtrage"),
|
||||||
|
PAGINATION("Pagination"),
|
||||||
|
UPDATE("Mise à jour"),
|
||||||
|
LOAD("Chargement");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
DataTableOperation(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
StringBuilder message = new StringBuilder(super.getMessage());
|
||||||
|
|
||||||
|
if (tableId != null) {
|
||||||
|
message.append(" [Table: ").append(tableId).append("]");
|
||||||
|
}
|
||||||
|
if (operation != null) {
|
||||||
|
message.append(" [Opération: ").append(operation.getLabel()).append("]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/main/java/dev/lions/exceptions/EmailException.java
Normal file
7
src/main/java/dev/lions/exceptions/EmailException.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
public class EmailException extends RuntimeException {
|
||||||
|
public EmailException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception levée lors d'erreurs de traitement des événements.
|
||||||
|
* Permet de gérer de manière cohérente les erreurs dans le système événementiel.
|
||||||
|
*/
|
||||||
|
public class EventProcessingException extends RuntimeException {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public EventProcessingException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventProcessingException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventProcessingException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||||
|
super(message, cause, enableSuppression, writableStackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception levée lors d'erreurs de publication d'événements analytiques.
|
||||||
|
*/
|
||||||
|
public class EventPublicationException extends RuntimeException {
|
||||||
|
|
||||||
|
public EventPublicationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventPublicationException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
135
src/main/java/dev/lions/exceptions/FileUploadException.java
Normal file
135
src/main/java/dev/lions/exceptions/FileUploadException.java
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception spécialisée pour la gestion des erreurs lors du téléchargement de fichiers.
|
||||||
|
* Cette classe encapsule les différents types d'erreurs pouvant survenir pendant
|
||||||
|
* le processus de téléchargement et de traitement des fichiers.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class FileUploadException extends BusinessException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détails techniques de l'erreur de téléchargement
|
||||||
|
*/
|
||||||
|
private final FileUploadErrorDetails errorDetails;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance avec un message d'erreur simple.
|
||||||
|
*
|
||||||
|
* @param message Description de l'erreur
|
||||||
|
*/
|
||||||
|
public FileUploadException(String message) {
|
||||||
|
this(message, null, null);
|
||||||
|
log.error("Erreur de téléchargement : {}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance avec un message et une cause.
|
||||||
|
*
|
||||||
|
* @param message Description de l'erreur
|
||||||
|
* @param cause Exception à l'origine de l'erreur
|
||||||
|
*/
|
||||||
|
public FileUploadException(String message, Throwable cause) {
|
||||||
|
this(message, cause, null);
|
||||||
|
log.error("Erreur de téléchargement : {}", message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance avec tous les détails de l'erreur.
|
||||||
|
*
|
||||||
|
* @param message Description de l'erreur
|
||||||
|
* @param cause Exception à l'origine de l'erreur
|
||||||
|
* @param errorDetails Détails techniques de l'erreur
|
||||||
|
*/
|
||||||
|
public FileUploadException(String message, Throwable cause, FileUploadErrorDetails errorDetails) {
|
||||||
|
super(message, cause);
|
||||||
|
this.errorDetails = errorDetails;
|
||||||
|
log.error("Erreur de téléchargement détaillée : {} - Détails : {}", message, errorDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les détails techniques de l'erreur.
|
||||||
|
*
|
||||||
|
* @return Détails de l'erreur ou null si non disponibles
|
||||||
|
*/
|
||||||
|
public FileUploadErrorDetails getErrorDetails() {
|
||||||
|
return errorDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classe interne représentant les détails techniques d'une erreur de téléchargement.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class FileUploadErrorDetails {
|
||||||
|
private final String fileName;
|
||||||
|
private final long fileSize;
|
||||||
|
private final String mimeType;
|
||||||
|
private final String uploadLocation;
|
||||||
|
private final String validationError;
|
||||||
|
private final String processingPhase;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format(
|
||||||
|
"FileUploadErrorDetails[fileName=%s, fileSize=%d, mimeType=%s, " +
|
||||||
|
"location=%s, error=%s, phase=%s]",
|
||||||
|
fileName, fileSize, mimeType, uploadLocation, validationError, processingPhase
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une instance d'exception pour un fichier trop volumineux.
|
||||||
|
*
|
||||||
|
* @param fileName Nom du fichier
|
||||||
|
* @param actualSize Taille réelle du fichier
|
||||||
|
* @param maxSize Taille maximale autorisée
|
||||||
|
* @return Instance de FileUploadException
|
||||||
|
*/
|
||||||
|
public static FileUploadException fileTooLarge(String fileName, long actualSize, long maxSize) {
|
||||||
|
String message = String.format(
|
||||||
|
"Le fichier '%s' est trop volumineux (%d octets). Maximum autorisé : %d octets",
|
||||||
|
fileName, actualSize, maxSize
|
||||||
|
);
|
||||||
|
|
||||||
|
FileUploadErrorDetails details = FileUploadErrorDetails.builder()
|
||||||
|
.fileName(fileName)
|
||||||
|
.fileSize(actualSize)
|
||||||
|
.validationError("FILE_TOO_LARGE")
|
||||||
|
.processingPhase("VALIDATION")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new FileUploadException(message, null, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une instance d'exception pour un type de fichier non autorisé.
|
||||||
|
*
|
||||||
|
* @param fileName Nom du fichier
|
||||||
|
* @param mimeType Type MIME du fichier
|
||||||
|
* @return Instance de FileUploadException
|
||||||
|
*/
|
||||||
|
public static FileUploadException invalidFileType(String fileName, String mimeType) {
|
||||||
|
String message = String.format(
|
||||||
|
"Le type de fichier '%s' n'est pas autorisé pour '%s'",
|
||||||
|
mimeType, fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
FileUploadErrorDetails details =
|
||||||
|
FileUploadErrorDetails.builder().fileName(fileName).mimeType(mimeType)
|
||||||
|
.validationError("INVALID_FILE_TYPE").processingPhase("VALIDATION")
|
||||||
|
.fileSize(-1).build();
|
||||||
|
|
||||||
|
return new FileUploadException(message, null, details);
|
||||||
|
}
|
||||||
|
}
|
||||||
158
src/main/java/dev/lions/exceptions/FilterException.java
Normal file
158
src/main/java/dev/lions/exceptions/FilterException.java
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception spécialisée pour la gestion des erreurs de filtrage.
|
||||||
|
* Cette classe encapsule les différentes erreurs pouvant survenir lors
|
||||||
|
* de l'application ou la manipulation des filtres de données.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class FilterException extends BusinessException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contexte détaillé de l'erreur de filtrage
|
||||||
|
*/
|
||||||
|
private final FilterContext filterContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance avec un message d'erreur simple.
|
||||||
|
*
|
||||||
|
* @param message Description de l'erreur
|
||||||
|
*/
|
||||||
|
public FilterException(String message) {
|
||||||
|
this(message, null, null);
|
||||||
|
log.error("Erreur de filtrage : {}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance avec un message et une cause.
|
||||||
|
*
|
||||||
|
* @param message Description de l'erreur
|
||||||
|
* @param cause Exception à l'origine de l'erreur
|
||||||
|
*/
|
||||||
|
public FilterException(String message, Throwable cause) {
|
||||||
|
this(message, cause, null);
|
||||||
|
log.error("Erreur de filtrage : {}", message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance avec tous les détails de l'erreur.
|
||||||
|
*
|
||||||
|
* @param message Description de l'erreur
|
||||||
|
* @param cause Exception à l'origine de l'erreur
|
||||||
|
* @param context Contexte du filtrage au moment de l'erreur
|
||||||
|
*/
|
||||||
|
public FilterException(String message, Throwable cause, FilterContext context) {
|
||||||
|
super(message, cause);
|
||||||
|
this.filterContext = context;
|
||||||
|
log.error("Erreur de filtrage détaillée : {} - Contexte : {}", message, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le contexte de l'erreur de filtrage.
|
||||||
|
*
|
||||||
|
* @return Contexte de l'erreur ou null si non disponible
|
||||||
|
*/
|
||||||
|
public FilterContext getFilterContext() {
|
||||||
|
return filterContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classe interne représentant le contexte d'une erreur de filtrage.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class FilterContext {
|
||||||
|
private final String field;
|
||||||
|
private final String operator;
|
||||||
|
private final String value;
|
||||||
|
private final String expectedType;
|
||||||
|
private final String actualType;
|
||||||
|
private final String validationError;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format(
|
||||||
|
"FilterContext[field=%s, operator=%s, value=%s, expectedType=%s, actualType=%s, error=%s]",
|
||||||
|
field, operator, value, expectedType, actualType, validationError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une exception pour un champ de filtrage invalide.
|
||||||
|
*
|
||||||
|
* @param fieldName Nom du champ
|
||||||
|
* @param value Valeur invalide
|
||||||
|
* @param expectedType Type attendu
|
||||||
|
* @return Instance de FilterException
|
||||||
|
*/
|
||||||
|
public static FilterException invalidFieldValue(String fieldName, String value, String expectedType) {
|
||||||
|
String message = String.format(
|
||||||
|
"Valeur invalide '%s' pour le champ '%s'. Type attendu : %s",
|
||||||
|
value, fieldName, expectedType
|
||||||
|
);
|
||||||
|
|
||||||
|
FilterContext context = FilterContext.builder()
|
||||||
|
.field(fieldName)
|
||||||
|
.value(value)
|
||||||
|
.expectedType(expectedType)
|
||||||
|
.validationError("INVALID_FIELD_VALUE")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new FilterException(message, null, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une exception pour un opérateur de filtre incompatible.
|
||||||
|
*
|
||||||
|
* @param operator Opérateur utilisé
|
||||||
|
* @param fieldName Nom du champ
|
||||||
|
* @param fieldType Type du champ
|
||||||
|
* @return Instance de FilterException
|
||||||
|
*/
|
||||||
|
public static FilterException incompatibleOperator(String operator, String fieldName, String fieldType) {
|
||||||
|
String message = String.format(
|
||||||
|
"L'opérateur '%s' n'est pas compatible avec le champ '%s' de type %s",
|
||||||
|
operator, fieldName, fieldType
|
||||||
|
);
|
||||||
|
|
||||||
|
FilterContext context = FilterContext.builder()
|
||||||
|
.field(fieldName)
|
||||||
|
.operator(operator)
|
||||||
|
.expectedType(fieldType)
|
||||||
|
.validationError("INCOMPATIBLE_OPERATOR")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new FilterException(message, null, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une exception pour une expression de filtre invalide.
|
||||||
|
*
|
||||||
|
* @param expression Expression de filtre
|
||||||
|
* @param reason Raison de l'invalidité
|
||||||
|
* @return Instance de FilterException
|
||||||
|
*/
|
||||||
|
public static FilterException invalidFilterExpression(String expression, String reason) {
|
||||||
|
String message = String.format(
|
||||||
|
"Expression de filtre invalide '%s' : %s",
|
||||||
|
expression, reason
|
||||||
|
);
|
||||||
|
|
||||||
|
FilterContext context = FilterContext.builder()
|
||||||
|
.value(expression)
|
||||||
|
.validationError("INVALID_FILTER_EXPRESSION")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new FilterException(message, null, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
public class ImageProcessingException extends BusinessException {
|
||||||
|
public ImageProcessingException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageProcessingException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/main/java/dev/lions/exceptions/IndexingException.java
Normal file
27
src/main/java/dev/lions/exceptions/IndexingException.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception personnalisée pour les erreurs d'indexation Elasticsearch.
|
||||||
|
* Cette exception est levée lorsqu'une opération d'indexation échoue.
|
||||||
|
*/
|
||||||
|
public class IndexingException extends RuntimeException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance avec un message d'erreur.
|
||||||
|
*
|
||||||
|
* @param message Le message décrivant l'erreur
|
||||||
|
*/
|
||||||
|
public IndexingException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance avec un message et une cause.
|
||||||
|
*
|
||||||
|
* @param message Le message décrivant l'erreur
|
||||||
|
* @param cause La cause originale de l'erreur
|
||||||
|
*/
|
||||||
|
public IndexingException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
210
src/main/java/dev/lions/exceptions/InitializationException.java
Normal file
210
src/main/java/dev/lions/exceptions/InitializationException.java
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception spécialisée pour la gestion des erreurs d'initialisation.
|
||||||
|
* Cette classe traite les erreurs survenant lors de l'initialisation
|
||||||
|
* des composants, services et ressources de l'application.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class InitializationException extends BusinessException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contexte détaillé de l'erreur d'initialisation
|
||||||
|
*/
|
||||||
|
private final InitializationContext context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase d'initialisation durant laquelle l'erreur est survenue
|
||||||
|
*/
|
||||||
|
private final InitializationPhase phase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance avec un message d'erreur simple.
|
||||||
|
*
|
||||||
|
* @param message Description de l'erreur
|
||||||
|
*/
|
||||||
|
public InitializationException(String message) {
|
||||||
|
this(message, null, null, null);
|
||||||
|
log.error("Erreur d'initialisation : {}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance avec un message et une cause.
|
||||||
|
*
|
||||||
|
* @param message Description de l'erreur
|
||||||
|
* @param cause Exception à l'origine de l'erreur
|
||||||
|
*/
|
||||||
|
public InitializationException(String message, Throwable cause) {
|
||||||
|
this(message, cause, null, null);
|
||||||
|
log.error("Erreur d'initialisation : {}", message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance avec tous les détails de l'erreur.
|
||||||
|
*
|
||||||
|
* @param message Description de l'erreur
|
||||||
|
* @param cause Exception à l'origine de l'erreur
|
||||||
|
* @param context Contexte de l'initialisation
|
||||||
|
* @param phase Phase d'initialisation
|
||||||
|
*/
|
||||||
|
public InitializationException(String message, Throwable cause,
|
||||||
|
InitializationContext context, InitializationPhase phase) {
|
||||||
|
super(message, cause);
|
||||||
|
this.context = context;
|
||||||
|
this.phase = phase;
|
||||||
|
log.error("Erreur d'initialisation détaillée : {} - Phase : {} - Contexte : {}",
|
||||||
|
message, phase, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le contexte de l'erreur d'initialisation.
|
||||||
|
*
|
||||||
|
* @return Contexte de l'erreur ou null si non disponible
|
||||||
|
*/
|
||||||
|
public InitializationContext getContext() {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la phase d'initialisation.
|
||||||
|
*
|
||||||
|
* @return Phase d'initialisation ou null si non disponible
|
||||||
|
*/
|
||||||
|
public InitializationPhase getPhase() {
|
||||||
|
return phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Représente les différentes phases d'initialisation possibles.
|
||||||
|
*/
|
||||||
|
public enum InitializationPhase {
|
||||||
|
CONFIGURATION("Configuration"),
|
||||||
|
RESOURCE_LOADING("Chargement des ressources"),
|
||||||
|
DATABASE("Base de données"),
|
||||||
|
DEPENDENCY_INJECTION("Injection de dépendances"),
|
||||||
|
SECURITY("Sécurité"),
|
||||||
|
CACHE("Cache"),
|
||||||
|
SERVICE_STARTUP("Démarrage des services");
|
||||||
|
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
InitializationPhase(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classe interne représentant le contexte d'une erreur d'initialisation.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class InitializationContext {
|
||||||
|
private final String componentName;
|
||||||
|
private final String resourceName;
|
||||||
|
private final String configurationKey;
|
||||||
|
private final String expectedState;
|
||||||
|
private final String actualState;
|
||||||
|
private final Map<String, String> additionalInfo;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder()
|
||||||
|
.append("InitializationContext[")
|
||||||
|
.append("component=").append(componentName)
|
||||||
|
.append(", resource=").append(resourceName)
|
||||||
|
.append(", config=").append(configurationKey);
|
||||||
|
|
||||||
|
if (expectedState != null) {
|
||||||
|
sb.append(", expected=").append(expectedState);
|
||||||
|
}
|
||||||
|
if (actualState != null) {
|
||||||
|
sb.append(", actual=").append(actualState);
|
||||||
|
}
|
||||||
|
if (additionalInfo != null && !additionalInfo.isEmpty()) {
|
||||||
|
sb.append(", info=").append(additionalInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.append("]").toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une exception pour une ressource manquante.
|
||||||
|
*
|
||||||
|
* @param resourceName Nom de la ressource
|
||||||
|
* @param componentName Nom du composant
|
||||||
|
* @return Instance de InitializationException
|
||||||
|
*/
|
||||||
|
public static InitializationException resourceNotFound(String resourceName, String componentName) {
|
||||||
|
String message = String.format(
|
||||||
|
"Ressource requise '%s' non trouvée pour le composant '%s'",
|
||||||
|
resourceName, componentName
|
||||||
|
);
|
||||||
|
|
||||||
|
InitializationContext context = InitializationContext.builder()
|
||||||
|
.componentName(componentName)
|
||||||
|
.resourceName(resourceName)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new InitializationException(message, null, context, InitializationPhase.RESOURCE_LOADING);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une exception pour une configuration invalide.
|
||||||
|
*
|
||||||
|
* @param key Clé de configuration
|
||||||
|
* @param expectedValue Valeur attendue
|
||||||
|
* @param actualValue Valeur actuelle
|
||||||
|
* @return Instance de InitializationException
|
||||||
|
*/
|
||||||
|
public static InitializationException invalidConfiguration(String key,
|
||||||
|
String expectedValue, String actualValue) {
|
||||||
|
String message = String.format(
|
||||||
|
"Configuration invalide pour '%s'. Attendu : %s, Actuel : %s",
|
||||||
|
key, expectedValue, actualValue
|
||||||
|
);
|
||||||
|
|
||||||
|
InitializationContext context = InitializationContext.builder()
|
||||||
|
.configurationKey(key)
|
||||||
|
.expectedState(expectedValue)
|
||||||
|
.actualState(actualValue)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new InitializationException(message, null, context, InitializationPhase.CONFIGURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une exception pour un échec de démarrage de service.
|
||||||
|
*
|
||||||
|
* @param serviceName Nom du service
|
||||||
|
* @param reason Raison de l'échec
|
||||||
|
* @return Instance de InitializationException
|
||||||
|
*/
|
||||||
|
public static InitializationException serviceStartupFailure(String serviceName, String reason) {
|
||||||
|
String message = String.format(
|
||||||
|
"Échec du démarrage du service '%s' : %s",
|
||||||
|
serviceName, reason
|
||||||
|
);
|
||||||
|
|
||||||
|
InitializationContext context = InitializationContext.builder()
|
||||||
|
.componentName(serviceName)
|
||||||
|
.additionalInfo(Map.of("reason", reason))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new InitializationException(message, null, context, InitializationPhase.SERVICE_STARTUP);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception spécifique pour les erreurs de conversion JSON.
|
||||||
|
*/
|
||||||
|
public class JsonConversionException extends RuntimeException {
|
||||||
|
|
||||||
|
public JsonConversionException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
public class NavigationException extends RuntimeException {
|
||||||
|
public NavigationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
public class NotificationException extends BusinessException {
|
||||||
|
public NotificationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NotificationException(String message, Throwable cause) { // Ajout du paramètre cause
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/main/java/dev/lions/exceptions/RepositoryException.java
Normal file
14
src/main/java/dev/lions/exceptions/RepositoryException.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception pour les erreurs de repository.
|
||||||
|
*/
|
||||||
|
public class RepositoryException extends RuntimeException {
|
||||||
|
public RepositoryException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RepositoryException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception levée lorsqu'une erreur de configuration de stockage se produit.
|
||||||
|
* Cette exception encapsule les erreurs liées à la configuration du stockage
|
||||||
|
* des fichiers, telles que des chemins de stockage invalides ou un espace
|
||||||
|
* de stockage insuffisant.
|
||||||
|
*/
|
||||||
|
public class StorageConfigurationException extends RuntimeException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance de StorageConfigurationException avec un message.
|
||||||
|
*
|
||||||
|
* @param message Message décrivant l'erreur de configuration du stockage
|
||||||
|
*/
|
||||||
|
public StorageConfigurationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance de StorageConfigurationException avec un message et une cause.
|
||||||
|
*
|
||||||
|
* @param message Message décrivant l'erreur de configuration du stockage
|
||||||
|
* @param cause Cause à l'origine de l'exception
|
||||||
|
*/
|
||||||
|
public StorageConfigurationException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/main/java/dev/lions/exceptions/TemplateException.java
Normal file
26
src/main/java/dev/lions/exceptions/TemplateException.java
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception pour les erreurs liées au traitement des templates.
|
||||||
|
*/
|
||||||
|
public class TemplateException extends RuntimeException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructeur avec un message.
|
||||||
|
*
|
||||||
|
* @param message Message de l'erreur
|
||||||
|
*/
|
||||||
|
public TemplateException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructeur avec un message et une cause.
|
||||||
|
*
|
||||||
|
* @param message Message de l'erreur
|
||||||
|
* @param cause Cause de l'erreur
|
||||||
|
*/
|
||||||
|
public TemplateException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
public class TemplateProcessingException extends Exception {
|
||||||
|
public TemplateProcessingException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TemplateProcessingException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package dev.lions.exceptions;
|
||||||
|
|
||||||
|
public class WebSocketException extends RuntimeException {
|
||||||
|
public WebSocketException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/main/java/dev/lions/health/ApplicationHealthCheck.java
Normal file
15
src/main/java/dev/lions/health/ApplicationHealthCheck.java
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package dev.lions.health;
|
||||||
|
|
||||||
|
import org.eclipse.microprofile.health.HealthCheck;
|
||||||
|
import org.eclipse.microprofile.health.HealthCheckResponse;
|
||||||
|
import org.eclipse.microprofile.health.Liveness;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
@Liveness
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ApplicationHealthCheck implements HealthCheck {
|
||||||
|
@Override
|
||||||
|
public HealthCheckResponse call() {
|
||||||
|
return HealthCheckResponse.up("Application health check");
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/main/java/dev/lions/models/Contact.java
Normal file
74
src/main/java/dev/lions/models/Contact.java
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package dev.lions.models;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Table (name = "contacts")
|
||||||
|
public class Contact {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue (strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Size (min = 2, max = 100)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@jakarta.validation.constraints.Email
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Size (max = 100)
|
||||||
|
private String company;
|
||||||
|
|
||||||
|
@Size (max = 20)
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Size (min = 3, max = 200)
|
||||||
|
private String subject;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Column (columnDefinition = "TEXT")
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Enumerated (EnumType.STRING)
|
||||||
|
private ContactStatus status;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private LocalDateTime submitDate;
|
||||||
|
|
||||||
|
private LocalDateTime processDate;
|
||||||
|
|
||||||
|
@Size (max = 500)
|
||||||
|
private String internalNotes;
|
||||||
|
|
||||||
|
public Contact(String name, String email, String subject, String message) {
|
||||||
|
this.name = name;
|
||||||
|
this.email = email;
|
||||||
|
this.subject = subject;
|
||||||
|
this.message = message;
|
||||||
|
this.status = ContactStatus.NEW;
|
||||||
|
this.submitDate = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
181
src/main/java/dev/lions/models/ContactForm.java
Normal file
181
src/main/java/dev/lions/models/ContactForm.java
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package dev.lions.models;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Représente un formulaire de contact.
|
||||||
|
* Gère les demandes de contact avec validation complète des données
|
||||||
|
* et traçabilité des soumissions.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ContactForm implements Serializable {
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private static final java.util.regex.Pattern PHONE_PATTERN = java.util.regex.Pattern.compile("^\\+?[0-9\\s-]{8,20}$");
|
||||||
|
private static final int MAX_COMPANY_LENGTH = 100;
|
||||||
|
private static final String DEFAULT_SUBJECT = "Demande d'information";
|
||||||
|
|
||||||
|
@NotNull (message = "Le nom est obligatoire")
|
||||||
|
@Size (min = 2, max = 100, message = "Le nom doit contenir entre 2 et 100 caractères")
|
||||||
|
@Pattern(regexp = "^[\\p{L}\\s'-]+$", message = "Le nom contient des caractères non autorisés")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@NotNull(message = "L'email est obligatoire")
|
||||||
|
@Email (message = "L'email n'est pas valide")
|
||||||
|
@Size(max = 100, message = "L'email ne doit pas dépasser 100 caractères")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@NotNull(message = "Le sujet est obligatoire")
|
||||||
|
@Size(min = 3, max = 100, message = "Le sujet doit contenir entre 3 et 100 caractères")
|
||||||
|
private String subject;
|
||||||
|
|
||||||
|
@NotNull(message = "Le message est obligatoire")
|
||||||
|
@Size(min = 10, max = 1000, message = "Le message doit contenir entre 10 et 1000 caractères")
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
@Size(max = MAX_COMPANY_LENGTH, message = "Le nom de l'entreprise ne doit pas dépasser 100 caractères")
|
||||||
|
private String company;
|
||||||
|
|
||||||
|
@Pattern(regexp = "^\\+?[0-9\\s-]{8,20}$", message = "Le format du numéro de téléphone n'est pas valide")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private LocalDateTime submitDate = LocalDateTime.now();
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private ContactStatus status = ContactStatus.NEW;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private Map<String, String> metadata = new HashMap<>();
|
||||||
|
|
||||||
|
private String ipAddress;
|
||||||
|
private String userAgent;
|
||||||
|
private String referer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une instance de base du formulaire.
|
||||||
|
*
|
||||||
|
* @param name Nom du contact
|
||||||
|
* @param email Email du contact
|
||||||
|
* @param message Message du contact
|
||||||
|
* @return Instance de ContactForm
|
||||||
|
*/
|
||||||
|
public static ContactForm createBasic(String name, String email, String message) {
|
||||||
|
return ContactForm.builder()
|
||||||
|
.name(name)
|
||||||
|
.email(email)
|
||||||
|
.subject(DEFAULT_SUBJECT)
|
||||||
|
.message(message)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize les données du formulaire.
|
||||||
|
* Nettoie et normalise les entrées utilisateur.
|
||||||
|
*/
|
||||||
|
public void sanitize() {
|
||||||
|
if (name != null) {
|
||||||
|
name = name.trim();
|
||||||
|
}
|
||||||
|
if (email != null) {
|
||||||
|
email = email.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
if (company != null) {
|
||||||
|
company = company.trim();
|
||||||
|
}
|
||||||
|
if (phone != null) {
|
||||||
|
phone = phone.replaceAll("[^+0-9\\s-]", "");
|
||||||
|
}
|
||||||
|
if (message != null) {
|
||||||
|
message = message.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le numéro de téléphone est valide.
|
||||||
|
*
|
||||||
|
* @return true si le format est valide
|
||||||
|
*/
|
||||||
|
public boolean isValidPhone() {
|
||||||
|
return phone == null || PHONE_PATTERN.matcher(phone).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une métadonnée au formulaire.
|
||||||
|
*
|
||||||
|
* @param key Clé de la métadonnée
|
||||||
|
* @param value Valeur de la métadonnée
|
||||||
|
*/
|
||||||
|
public void addMetadata(String key, String value) {
|
||||||
|
if (key != null && value != null) {
|
||||||
|
metadata.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le statut du formulaire.
|
||||||
|
*
|
||||||
|
* @param newStatus Nouveau statut
|
||||||
|
* @param reason Raison du changement (optionnel)
|
||||||
|
*/
|
||||||
|
public void updateStatus(ContactStatus newStatus, String reason) {
|
||||||
|
this.status = newStatus;
|
||||||
|
if (reason != null) {
|
||||||
|
addMetadata("statusChangeReason", reason);
|
||||||
|
addMetadata("statusChangeDate", LocalDateTime.now().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le formulaire est complet et valide.
|
||||||
|
*
|
||||||
|
* @return true si le formulaire est valide
|
||||||
|
*/
|
||||||
|
public boolean isValid() {
|
||||||
|
return name != null && !name.trim().isEmpty() &&
|
||||||
|
email != null && email.contains("@") &&
|
||||||
|
message != null && message.trim().length() >= 10 &&
|
||||||
|
isValidPhone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une représentation du formulaire pour les logs.
|
||||||
|
*
|
||||||
|
* @return Version sécurisée pour les logs
|
||||||
|
*/
|
||||||
|
public String toLogString() {
|
||||||
|
return String.format("ContactForm[name=%s, email=%s, subject=%s, timestamp=%s, status=%s]",
|
||||||
|
name,
|
||||||
|
email.replaceAll("(?<=.{3}).(?=.*@)", "*"),
|
||||||
|
subject,
|
||||||
|
submitDate,
|
||||||
|
status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le formulaire nécessite une attention urgente.
|
||||||
|
*
|
||||||
|
* @return true si urgent
|
||||||
|
*/
|
||||||
|
public boolean isUrgent() {
|
||||||
|
return subject != null &&
|
||||||
|
(subject.toLowerCase().contains("urgent") ||
|
||||||
|
message.toLowerCase().contains("urgent"));
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/main/java/dev/lions/models/ContactStatus.java
Normal file
31
src/main/java/dev/lions/models/ContactStatus.java
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package dev.lions.models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Énumération représentant les différents statuts possibles
|
||||||
|
* pour un formulaire de contact.
|
||||||
|
*/
|
||||||
|
public enum ContactStatus {
|
||||||
|
NEW("Nouveau"),
|
||||||
|
IN_PROGRESS("En cours de traitement"),
|
||||||
|
RESPONDED("Répondu"),
|
||||||
|
CLOSED("Clôturé"),
|
||||||
|
SPAM("Spam");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructeur privé pour initialiser le libellé du statut.
|
||||||
|
* @param label Libellé du statut
|
||||||
|
*/
|
||||||
|
private ContactStatus(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le libellé du statut.
|
||||||
|
* @return Libellé du statut
|
||||||
|
*/
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/main/java/dev/lions/models/EmailMessage.java
Normal file
16
src/main/java/dev/lions/models/EmailMessage.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package dev.lions.models;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classe représentant un message email.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class EmailMessage {
|
||||||
|
private final String from;
|
||||||
|
private final String to;
|
||||||
|
private final String subject;
|
||||||
|
private final String htmlContent;
|
||||||
|
}
|
||||||
85
src/main/java/dev/lions/models/EmailTemplate.java
Normal file
85
src/main/java/dev/lions/models/EmailTemplate.java
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package dev.lions.models;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Collections;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Représente un modèle de email à utiliser pour l'envoi de communications.
|
||||||
|
* Cette classe encapsule les informations nécessaires pour générer et envoyer un email.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class EmailTemplate {
|
||||||
|
|
||||||
|
@NotBlank(message = "L'identifiant du modèle de courriel est obligatoire")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@NotBlank(message = "Le nom du modèle de courriel est obligatoire")
|
||||||
|
@Size(max = 100, message = "Le nom du modèle ne peut pas dépasser 100 caractères")
|
||||||
|
private String templateName;
|
||||||
|
|
||||||
|
@NotBlank(message = "L'objet du courriel est obligatoire")
|
||||||
|
@Size(max = 100, message = "L'objet ne peut pas dépasser 100 caractères")
|
||||||
|
private String subject;
|
||||||
|
|
||||||
|
@NotBlank(message = "Le destinataire du courriel est obligatoire")
|
||||||
|
@Email(message = "Le destinataire doit être une adresse email valide")
|
||||||
|
private String recipient;
|
||||||
|
|
||||||
|
private Map<String, String> parameters;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private boolean isActive = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -- SETTER --
|
||||||
|
* Met à jour la version du modèle de courriel.
|
||||||
|
*
|
||||||
|
* @param version Nouvelle version
|
||||||
|
*/
|
||||||
|
@Setter
|
||||||
|
@Builder.Default
|
||||||
|
private long version = 0;
|
||||||
|
|
||||||
|
@NotBlank(message = "Le contenu du courriel est obligatoire")
|
||||||
|
@Size(max = 10000, message = "Le contenu ne peut pas dépasser 10 000 caractères")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une copie immuable des paramètres.
|
||||||
|
*
|
||||||
|
* @return Paramètres du modèle de courriel
|
||||||
|
*/
|
||||||
|
public Map<String, String> getParameters() {
|
||||||
|
return Collections.unmodifiableMap(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour l'état d'activation du modèle de courriel.
|
||||||
|
*
|
||||||
|
* @param active Nouvel état d'activation
|
||||||
|
*/
|
||||||
|
public void setActive(boolean active) {
|
||||||
|
this.isActive = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le modèle de courriel est valide et prêt à l'emploi.
|
||||||
|
*
|
||||||
|
* @return true si le modèle est valide
|
||||||
|
*/
|
||||||
|
public boolean isValid() {
|
||||||
|
return id != null &&
|
||||||
|
templateName != null && !templateName.isBlank() &&
|
||||||
|
subject != null && !subject.isBlank() &&
|
||||||
|
recipient != null && !recipient.isBlank() &&
|
||||||
|
content != null && !content.isBlank();
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/main/java/dev/lions/models/ExpertiseArea.java
Normal file
54
src/main/java/dev/lions/models/ExpertiseArea.java
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package dev.lions.models;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Représente un domaine d'expertise de l'entreprise.
|
||||||
|
* Chaque domaine d'expertise est caractérisé par un titre, une icône,
|
||||||
|
* une description et une liste de fonctionnalités associées.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ExpertiseArea implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Titre du domaine d'expertise, compris entre 3 et 100 caractères.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
@Size(min = 3, max = 100)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icône associée au domaine d'expertise, comprise entre 3 et 50 caractères.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
@Size(min = 3, max = 50)
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Description du domaine d'expertise, comprise entre 10 et 500 caractères.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
@Size(min = 10, max = 500)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste des fonctionnalités associées au domaine d'expertise.
|
||||||
|
*/
|
||||||
|
private List<String> features;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Priorité du domaine d'expertise.
|
||||||
|
*/
|
||||||
|
private int priority;
|
||||||
|
}
|
||||||
127
src/main/java/dev/lions/models/Notification.java
Normal file
127
src/main/java/dev/lions/models/Notification.java
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package dev.lions.models;
|
||||||
|
|
||||||
|
import dev.lions.utils.JsonConverter;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Convert;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Représente une notification système à destination d'un utilisateur.
|
||||||
|
* Cette classe encapsule les informations nécessaires pour générer, stocker
|
||||||
|
* et afficher une notification dans l'application.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "notifications")
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class Notification {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 1000)
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private NotificationType type;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private NotificationStatus status;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
|
||||||
|
@Column(name = "target_user_id")
|
||||||
|
private Long targetUserId;
|
||||||
|
|
||||||
|
@Column(name = "source_entity_type")
|
||||||
|
private String sourceEntityType;
|
||||||
|
|
||||||
|
@Column(name = "source_entity_id")
|
||||||
|
private Long sourceEntityId;
|
||||||
|
|
||||||
|
@Column(name = "read_timestamp")
|
||||||
|
private LocalDateTime readTimestamp;
|
||||||
|
|
||||||
|
@Column(name = "action_url")
|
||||||
|
private String actionUrl;
|
||||||
|
|
||||||
|
@Column(name = "notification_data", columnDefinition = "jsonb")
|
||||||
|
@Convert(converter = JsonConverter.class)
|
||||||
|
private NotificationData data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les valeurs par défaut de la notification.
|
||||||
|
* La date de création et le statut "non lu" sont définis ici.
|
||||||
|
*/
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
if (timestamp == null) {
|
||||||
|
timestamp = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
if (status == null) {
|
||||||
|
status = NotificationStatus.UNREAD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Représente les données supplémentaires associées à la notification.
|
||||||
|
* Cette classe imbriquée permet de stocker des attributs et métadonnées.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class NotificationData {
|
||||||
|
private Map<String, Object> attributes;
|
||||||
|
private Map<String, String> metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si la notification a été marquée comme lue.
|
||||||
|
*
|
||||||
|
* @return true si la notification a été lue
|
||||||
|
*/
|
||||||
|
public boolean isRead() {
|
||||||
|
return NotificationStatus.READ.equals(this.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si la notification est de type critique.
|
||||||
|
*
|
||||||
|
* @return true si la notification est critique
|
||||||
|
*/
|
||||||
|
public boolean isCritical() {
|
||||||
|
return type != null && type.isCritical();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marque la notification comme lue.
|
||||||
|
* Met à jour le statut et la date de lecture.
|
||||||
|
*/
|
||||||
|
public void markAsRead() {
|
||||||
|
this.status = NotificationStatus.READ;
|
||||||
|
this.readTimestamp = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/main/java/dev/lions/models/NotificationStatus.java
Normal file
88
src/main/java/dev/lions/models/NotificationStatus.java
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package dev.lions.models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Représente les différents statuts possibles pour une notification système.
|
||||||
|
* Chaque statut est associé à une étiquette lisible et une classe CSS
|
||||||
|
* pour la mise en forme de l'interface utilisateur.
|
||||||
|
*/
|
||||||
|
public enum NotificationStatus {
|
||||||
|
UNREAD("Non lu", "notification-unread"),
|
||||||
|
READ("Lu", "notification-read"),
|
||||||
|
ARCHIVED("Archivé", "notification-archived"),
|
||||||
|
DELETED("Supprimé", "notification-deleted"),
|
||||||
|
PENDING("En attente", "notification-pending"),
|
||||||
|
PROCESSING("En cours de traitement", "notification-processing"),
|
||||||
|
ERROR("Erreur", "notification-error");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
private final String cssClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructeur privé pour créer une instance de NotificationStatus.
|
||||||
|
*
|
||||||
|
* @param label Étiquette lisible du statut
|
||||||
|
* @param cssClass Classe CSS pour la mise en forme
|
||||||
|
*/
|
||||||
|
private NotificationStatus(String label, String cssClass) {
|
||||||
|
this.label = label;
|
||||||
|
this.cssClass = cssClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'étiquette lisible du statut.
|
||||||
|
*
|
||||||
|
* @return Étiquette du statut
|
||||||
|
*/
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la classe CSS associée au statut.
|
||||||
|
* Cette classe peut être utilisée pour la mise en forme de l'interface utilisateur.
|
||||||
|
*
|
||||||
|
* @return Classe CSS du statut
|
||||||
|
*/
|
||||||
|
public String getCssClass() {
|
||||||
|
return cssClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le statut correspond à une notification active.
|
||||||
|
* Les notifications archivées ou supprimées ne sont pas considérées comme actives.
|
||||||
|
*
|
||||||
|
* @return true si la notification est active
|
||||||
|
*/
|
||||||
|
public boolean isActive() {
|
||||||
|
return this != ARCHIVED && this != DELETED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le statut indique que la notification nécessite une attention particulière.
|
||||||
|
* Les notifications non lues ou en erreur sont considérées comme nécessitant une attention.
|
||||||
|
*
|
||||||
|
* @return true si la notification nécessite une attention
|
||||||
|
*/
|
||||||
|
public boolean requiresAttention() {
|
||||||
|
return this == UNREAD || this == ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si la transition vers un nouveau statut est autorisée.
|
||||||
|
* Les règles de transition sont définies en fonction de l'état actuel.
|
||||||
|
*
|
||||||
|
* @param newStatus Nouveau statut à atteindre
|
||||||
|
* @return true si la transition est autorisée
|
||||||
|
*/
|
||||||
|
public boolean canTransitionTo(NotificationStatus newStatus) {
|
||||||
|
if (this == DELETED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this == ARCHIVED && newStatus != DELETED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/main/java/dev/lions/models/NotificationType.java
Normal file
59
src/main/java/dev/lions/models/NotificationType.java
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package dev.lions.models;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Représente les différents types de notifications utilisées dans l'application.
|
||||||
|
* Chaque type de notification est associé à un titre, un message par défaut et
|
||||||
|
* un indicateur de criticité.
|
||||||
|
*/
|
||||||
|
public enum NotificationType {
|
||||||
|
NEW_CONTACT(true, "Nouveau contact", "Un nouveau contact a été reçu"),
|
||||||
|
PROJECT_UPDATE(false, "Mise à jour projet", "Un projet a été mis à jour"),
|
||||||
|
TASK_ASSIGNED(true, "Tâche assignée", "Une nouvelle tâche vous a été assignée"),
|
||||||
|
COMMENT_ADDED(false, "Nouveau commentaire", "Un commentaire a été ajouté"),
|
||||||
|
DEADLINE_APPROACHING(true, "Échéance proche", "Une échéance approche"),
|
||||||
|
SYSTEM_ALERT(true, "Alerte système", "Une alerte système requiert votre attention"),
|
||||||
|
MAINTENANCE_SCHEDULED(false, "Maintenance planifiée", "Une maintenance est planifiée"),
|
||||||
|
USER_MENTION(true, "Mention", "Vous avez été mentionné"),
|
||||||
|
SECURITY_ALERT(true, "Alerte sécurité", "Un problème de sécurité a été détecté"),
|
||||||
|
RESOURCE_LIMIT(true, "Limite ressources", "Une limite de ressources a été atteinte");
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final String title;
|
||||||
|
@Getter
|
||||||
|
private final String defaultMessage;
|
||||||
|
private final boolean isCritical;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructeur privé pour créer une instance de NotificationType.
|
||||||
|
*
|
||||||
|
* @param isCritical Indicateur de criticité de la notification
|
||||||
|
* @param title Titre de la notification
|
||||||
|
* @param defaultMessage Message par défaut de la notification
|
||||||
|
*/
|
||||||
|
private NotificationType(boolean isCritical, String title, String defaultMessage) {
|
||||||
|
this.isCritical = isCritical;
|
||||||
|
this.title = title;
|
||||||
|
this.defaultMessage = defaultMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le modèle de notification sous forme de chaîne de caractères.
|
||||||
|
*
|
||||||
|
* @return Modèle de notification avec le titre et le message par défaut
|
||||||
|
*/
|
||||||
|
public String getNotificationTemplate() {
|
||||||
|
return String.format("%s : %s", this.title, this.defaultMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indique si le type de notification est critique.
|
||||||
|
* Les notifications critiques nécessitent généralement une attention prioritaire.
|
||||||
|
*
|
||||||
|
* @return true si le type de notification est critique
|
||||||
|
*/
|
||||||
|
public boolean isCritical() {
|
||||||
|
return isCritical;
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/main/java/dev/lions/models/ProcessStep.java
Normal file
51
src/main/java/dev/lions/models/ProcessStep.java
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package dev.lions.models;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Représente une étape du processus de réalisation.
|
||||||
|
* Chaque étape est caractérisée par un numéro, un titre et une description.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ProcessStep implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Numéro de l'étape du processus.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private int number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Titre de l'étape, compris entre 3 et 50 caractères.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
@Size(min = 3, max = 50)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Description de l'étape, comprise entre 10 et 250 caractères.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
@Size(min = 10, max = 250)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détails supplémentaires de l'étape.
|
||||||
|
*/
|
||||||
|
private String details;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duree de l'étape.
|
||||||
|
*/
|
||||||
|
private String duration;
|
||||||
|
}
|
||||||
215
src/main/java/dev/lions/models/Project.java
Normal file
215
src/main/java/dev/lions/models/Project.java
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
package dev.lions.models;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.*;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.hibernate.annotations.Cache;
|
||||||
|
import org.hibernate.annotations.CacheConcurrencyStrategy;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entité représentant un projet dans le système.
|
||||||
|
* Gère les informations complètes d'un projet, incluant ses métadonnées,
|
||||||
|
* technologies, témoignages et statut.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(
|
||||||
|
name = "projects",
|
||||||
|
indexes = {
|
||||||
|
@Index(name = "idx_project_completion_date", columnList = "completionDate"),
|
||||||
|
@Index(name = "idx_project_featured", columnList = "featured")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class Project implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@NotNull(message = "L'identifiant du projet est obligatoire")
|
||||||
|
@Pattern(regexp = "^[a-zA-Z0-9-_]+$", message = "L'identifiant ne doit contenir que des lettres, chiffres, tirets et underscores")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 100)
|
||||||
|
@NotNull(message = "Le titre du projet est obligatoire")
|
||||||
|
@Size(min = 3, max = 100, message = "Le titre doit contenir entre 3 et 100 caractères")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 500)
|
||||||
|
@NotNull(message = "La description du projet est obligatoire")
|
||||||
|
@Size(min = 10, max = 500, message = "La description doit contenir entre 10 et 500 caractères")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 250)
|
||||||
|
@NotNull(message = "La description courte est obligatoire")
|
||||||
|
@Size(min = 10, max = 250, message = "La description courte doit contenir entre 10 et 250 caractères")
|
||||||
|
private String shortDescription;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@NotNull(message = "L'URL de l'image est obligatoire")
|
||||||
|
@Pattern(regexp = "^[^<>\"']*$", message = "L'URL de l'image contient des caractères non autorisés")
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
@Column(length = 100)
|
||||||
|
@Pattern(regexp = "^[^<>\"']*$", message = "Le nom du client contient des caractères non autorisés")
|
||||||
|
private String clientName;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@PastOrPresent(message = "La date de réalisation ne peut pas être dans le futur")
|
||||||
|
private LocalDateTime completionDate;
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(
|
||||||
|
name = "project_tags",
|
||||||
|
joinColumns = @JoinColumn(name = "project_id")
|
||||||
|
)
|
||||||
|
@Column(name = "tag", length = 50)
|
||||||
|
@Builder.Default
|
||||||
|
private List<@Pattern(regexp = "^[a-zA-Z0-9-_]+$") String> tags = new ArrayList<>();
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(
|
||||||
|
name = "project_technologies",
|
||||||
|
joinColumns = @JoinColumn(name = "project_id")
|
||||||
|
)
|
||||||
|
@Column(name = "technology", length = 50)
|
||||||
|
@Builder.Default
|
||||||
|
private List<@Pattern(regexp = "^[a-zA-Z0-9-_. ]+$") String> technologies = new ArrayList<>();
|
||||||
|
|
||||||
|
@Column(length = 1000)
|
||||||
|
@Size(max = 1000, message = "La description du challenge ne doit pas dépasser 1000 caractères")
|
||||||
|
private String challenge;
|
||||||
|
|
||||||
|
@Column(length = 1000)
|
||||||
|
@Size(max = 1000, message = "La description de la solution ne doit pas dépasser 1000 caractères")
|
||||||
|
private String solution;
|
||||||
|
|
||||||
|
@Column(length = 1000)
|
||||||
|
@Size(max = 1000, message = "La description des résultats ne doit pas dépasser 1000 caractères")
|
||||||
|
private String results;
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(
|
||||||
|
name = "project_testimonials",
|
||||||
|
joinColumns = @JoinColumn(name = "project_id")
|
||||||
|
)
|
||||||
|
@Column(name = "testimonial", length = 1000)
|
||||||
|
@Builder.Default
|
||||||
|
private List<@Size(max = 1000) String> testimonials = new ArrayList<>();
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private boolean featured = false;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private Long version;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
@CreationTimestamp
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
@UpdateTimestamp
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les tags de manière sécurisée.
|
||||||
|
*
|
||||||
|
* @return Liste immuable des tags
|
||||||
|
*/
|
||||||
|
public List<String> getTags() {
|
||||||
|
return Collections.unmodifiableList(tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les technologies de manière sécurisée.
|
||||||
|
*
|
||||||
|
* @return Liste immuable des technologies
|
||||||
|
*/
|
||||||
|
public List<String> getTechnologies() {
|
||||||
|
return Collections.unmodifiableList(technologies);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les témoignages de manière sécurisée.
|
||||||
|
*
|
||||||
|
* @return Liste immuable des témoignages
|
||||||
|
*/
|
||||||
|
public List<String> getTestimonials() {
|
||||||
|
return Collections.unmodifiableList(testimonials);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute un tag au projet.
|
||||||
|
*
|
||||||
|
* @param tag Tag à ajouter
|
||||||
|
* @return true si le tag a été ajouté
|
||||||
|
*/
|
||||||
|
public boolean addTag(String tag) {
|
||||||
|
if (tag != null && !tag.isEmpty() && !tags.contains(tag)) {
|
||||||
|
return tags.add(tag.trim().toLowerCase());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une technologie au projet.
|
||||||
|
*
|
||||||
|
* @param technology Technologie à ajouter
|
||||||
|
* @return true si la technologie a été ajoutée
|
||||||
|
*/
|
||||||
|
public boolean addTechnology(String technology) {
|
||||||
|
if (technology != null && !technology.isEmpty() && !technologies.contains(technology)) {
|
||||||
|
return technologies.add(technology.trim());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute un témoignage au projet.
|
||||||
|
*
|
||||||
|
* @param testimonial Témoignage à ajouter
|
||||||
|
* @return true si le témoignage a été ajouté
|
||||||
|
*/
|
||||||
|
public boolean addTestimonial(String testimonial) {
|
||||||
|
if (testimonial != null && !testimonial.isEmpty()) {
|
||||||
|
return testimonials.add(testimonial.trim());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le premier témoignage s'il existe.
|
||||||
|
*
|
||||||
|
* @return Optional contenant le premier témoignage
|
||||||
|
*/
|
||||||
|
public Optional<String> getFirstTestimonial() {
|
||||||
|
return testimonials.isEmpty() ? Optional.empty() :
|
||||||
|
Optional.of(testimonials.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le projet est complet et prêt à être publié.
|
||||||
|
*
|
||||||
|
* @return true si le projet est complet
|
||||||
|
*/
|
||||||
|
public boolean isComplete() {
|
||||||
|
return id != null && !id.isEmpty() &&
|
||||||
|
title != null && !title.isEmpty() &&
|
||||||
|
description != null && !description.isEmpty() &&
|
||||||
|
imageUrl != null && !imageUrl.isEmpty() &&
|
||||||
|
completionDate != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
181
src/main/java/dev/lions/models/ProjectImage.java
Normal file
181
src/main/java/dev/lions/models/ProjectImage.java
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package dev.lions.models;
|
||||||
|
|
||||||
|
import dev.lions.utils.ImageType;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.*;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entité représentant une image associée à un projet.
|
||||||
|
* Cette classe gère les métadonnées et le stockage des images
|
||||||
|
* avec support pour différents types et versions.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "project_images")
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ProjectImage implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private static final int MAX_FILE_SIZE = 10485760; // 10MB
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "project_id", nullable = false)
|
||||||
|
@NotNull(message = "Le projet associé est requis")
|
||||||
|
private Project project;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@NotBlank(message = "Le nom du fichier est requis")
|
||||||
|
@Size(max = 255, message = "Le nom du fichier ne peut pas dépasser 255 caractères")
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "type_id", nullable = false)
|
||||||
|
@NotNull(message = "Le type d'image est requis")
|
||||||
|
private ImageType type;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@Min(value = 1, message = "La largeur doit être positive")
|
||||||
|
@Max(value = 10000, message = "La largeur ne peut pas dépasser 10000 pixels")
|
||||||
|
private Integer width;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@Min(value = 1, message = "La hauteur doit être positive")
|
||||||
|
@Max(value = 10000, message = "La hauteur ne peut pas dépasser 10000 pixels")
|
||||||
|
private Integer height;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@Min(value = 1, message = "La taille du fichier doit être positive")
|
||||||
|
@Max(value = MAX_FILE_SIZE, message = "La taille du fichier ne peut pas dépasser 10MB")
|
||||||
|
private Long fileSize;
|
||||||
|
|
||||||
|
@Column(length = 500)
|
||||||
|
@Size(max = 500, message = "Le texte alternatif ne peut pas dépasser 500 caractères")
|
||||||
|
private String altText;
|
||||||
|
|
||||||
|
@Column(name = "mime_type")
|
||||||
|
@NotBlank(message = "Le type MIME est requis")
|
||||||
|
@Pattern(regexp = "^image/[a-zA-Z0-9.+-]+$",
|
||||||
|
message = "Type MIME invalide")
|
||||||
|
private String mimeType;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime uploadDate;
|
||||||
|
|
||||||
|
@Column(name = "last_modified")
|
||||||
|
private LocalDateTime lastModified;
|
||||||
|
|
||||||
|
@Column(name = "checksum")
|
||||||
|
@NotBlank(message = "Le checksum est requis")
|
||||||
|
private String checksum;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private Long version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les champs par défaut avant la persistance.
|
||||||
|
*/
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
if (uploadDate == null) {
|
||||||
|
uploadDate = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
lastModified = uploadDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour la date de modification avant la mise à jour.
|
||||||
|
*/
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
lastModified = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule le ratio d'aspect de l'image.
|
||||||
|
*
|
||||||
|
* @return Ratio largeur/hauteur
|
||||||
|
*/
|
||||||
|
public double getAspectRatio() {
|
||||||
|
return width.doubleValue() / height.doubleValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'image est en mode portrait.
|
||||||
|
*
|
||||||
|
* @return true si la hauteur est supérieure à la largeur
|
||||||
|
*/
|
||||||
|
public boolean isPortrait() {
|
||||||
|
return height > width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'image est en mode paysage.
|
||||||
|
*
|
||||||
|
* @return true si la largeur est supérieure à la hauteur
|
||||||
|
*/
|
||||||
|
public boolean isLandscape() {
|
||||||
|
return width > height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'image est carrée.
|
||||||
|
*
|
||||||
|
* @return true si la largeur est égale à la hauteur
|
||||||
|
*/
|
||||||
|
public boolean isSquare() {
|
||||||
|
return width.equals(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère une URL relative pour l'image.
|
||||||
|
*
|
||||||
|
* @return URL relative de l'image
|
||||||
|
*/
|
||||||
|
public String getRelativeUrl() {
|
||||||
|
return String.format("/images/projects/%d/%s", project.getId(), fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si la taille de l'image est valide.
|
||||||
|
*
|
||||||
|
* @return true si les dimensions sont valides
|
||||||
|
*/
|
||||||
|
public boolean hasValidDimensions() {
|
||||||
|
return width > 0 && width <= 10000 &&
|
||||||
|
height > 0 && height <= 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si la taille du fichier est valide.
|
||||||
|
*
|
||||||
|
* @return true si la taille est valide
|
||||||
|
*/
|
||||||
|
public boolean hasValidFileSize() {
|
||||||
|
return fileSize > 0 && fileSize <= MAX_FILE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule la taille en mégaoctets.
|
||||||
|
*
|
||||||
|
* @return Taille en Mo
|
||||||
|
*/
|
||||||
|
public double getSizeInMB() {
|
||||||
|
return fileSize / (1024.0 * 1024.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
179
src/main/java/dev/lions/models/Service.java
Normal file
179
src/main/java/dev/lions/models/Service.java
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package dev.lions.models;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Représente un service proposé par l'entreprise.
|
||||||
|
* Cette classe définit les caractéristiques et fonctionnalités d'un service,
|
||||||
|
* avec validation complète des données et gestion des états.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class Service implements Serializable, Comparable<Service> {
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@NotNull(message = "L'identifiant du service ne peut pas être nul")
|
||||||
|
@Pattern (regexp = "^[a-z0-9-]+$", message = "L'identifiant doit être en minuscules, avec chiffres et tirets uniquement")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@NotNull(message = "Le titre du service ne peut pas être nul")
|
||||||
|
@Size(min = 3, max = 100, message = "Le titre doit contenir entre 3 et 100 caractères")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@NotNull(message = "L'icône du service ne peut pas être nulle")
|
||||||
|
@Pattern(regexp = "^fa-[a-z0-9-]+$", message = "L'icône doit suivre le format Font Awesome (ex: fa-users)")
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
@NotNull(message = "La description du service ne peut pas être nulle")
|
||||||
|
@Size(min = 10, max = 500, message = "La description doit contenir entre 10 et 500 caractères")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Size(max = 1000, message = "La description détaillée ne doit pas dépasser 1000 caractères")
|
||||||
|
private String longDescription;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private List<@Size(max = 100) String> benefits = new ArrayList<>();
|
||||||
|
|
||||||
|
@Pattern(regexp = "^/[a-z0-9-/]+$", message = "L'URL doit commencer par '/' et ne contenir que des caractères valides")
|
||||||
|
private String detailsUrl;
|
||||||
|
|
||||||
|
@Min (value = 0, message = "La priorité doit être positive")
|
||||||
|
@Max (value = 100, message = "La priorité ne peut pas dépasser 100")
|
||||||
|
@Builder.Default
|
||||||
|
private int priority = 50;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private boolean isActive = true;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
|
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@Pattern(regexp = "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", message = "La couleur doit être au format hexadécimal")
|
||||||
|
@Builder.Default
|
||||||
|
private String accentColor = "#2196F3";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un service de base avec les champs obligatoires.
|
||||||
|
*
|
||||||
|
* @param title Titre du service
|
||||||
|
* @param icon Icône du service
|
||||||
|
* @param description Description du service
|
||||||
|
* @param benefits Liste des avantages
|
||||||
|
* @param detailsUrl URL des détails
|
||||||
|
* @return Instance de Service
|
||||||
|
*/
|
||||||
|
public static Service createBasicService(String title, String icon, String description,
|
||||||
|
List<String> benefits, String detailsUrl) {
|
||||||
|
String id = title.toLowerCase()
|
||||||
|
.replaceAll("[^a-z0-9-]", "-")
|
||||||
|
.replaceAll("-+", "-");
|
||||||
|
|
||||||
|
return Service.builder()
|
||||||
|
.id(id)
|
||||||
|
.title(title)
|
||||||
|
.icon(icon)
|
||||||
|
.description(description)
|
||||||
|
.benefits(benefits != null ? new ArrayList<>(benefits) : new ArrayList<>())
|
||||||
|
.detailsUrl(detailsUrl)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute un avantage à la liste des bénéfices.
|
||||||
|
*
|
||||||
|
* @param benefit Avantage à ajouter
|
||||||
|
* @return true si l'ajout a réussi
|
||||||
|
*/
|
||||||
|
public boolean addBenefit(String benefit) {
|
||||||
|
if (benefit != null && !benefit.isEmpty() && benefit.length() <= 100) {
|
||||||
|
return benefits.add(benefit.trim());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la liste immuable des avantages.
|
||||||
|
*
|
||||||
|
* @return Liste des avantages
|
||||||
|
*/
|
||||||
|
public List<String> getBenefits() {
|
||||||
|
return Collections.unmodifiableList(benefits);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour la date de modification.
|
||||||
|
*/
|
||||||
|
public void touch() {
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active ou désactive le service.
|
||||||
|
*
|
||||||
|
* @param active Nouvel état
|
||||||
|
*/
|
||||||
|
public void setActive(boolean active) {
|
||||||
|
this.isActive = active;
|
||||||
|
touch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le service est complet et valide.
|
||||||
|
*
|
||||||
|
* @return true si le service est valide
|
||||||
|
*/
|
||||||
|
public boolean isValid() {
|
||||||
|
return id != null && !id.isEmpty() &&
|
||||||
|
title != null && !title.isEmpty() &&
|
||||||
|
icon != null && !icon.isEmpty() &&
|
||||||
|
description != null && !description.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implémente la comparaison pour le tri par priorité.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int compareTo(Service other) {
|
||||||
|
return Integer.compare(this.priority, other.priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit une représentation HTML sûre de l'icône.
|
||||||
|
*
|
||||||
|
* @return Balise HTML de l'icône
|
||||||
|
*/
|
||||||
|
public String getIconHtml() {
|
||||||
|
return String.format("<i class=\"fas %s\" aria-hidden=\"true\"></i>",
|
||||||
|
icon.replaceAll("[^a-zA-Z0-9-]", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un identifiant unique basé sur le titre.
|
||||||
|
*/
|
||||||
|
public void generateId() {
|
||||||
|
if (this.id == null || this.id.isEmpty()) {
|
||||||
|
this.id = this.title.toLowerCase()
|
||||||
|
.replaceAll("[^a-z0-9-]", "-")
|
||||||
|
.replaceAll("-+", "-");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
150
src/main/java/dev/lions/models/Testimonial.java
Normal file
150
src/main/java/dev/lions/models/Testimonial.java
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package dev.lions.models;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Objects;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
import org.hibernate.annotations.Cache;
|
||||||
|
import org.hibernate.annotations.CacheConcurrencyStrategy;
|
||||||
|
import org.hibernate.proxy.HibernateProxy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Représente un témoignage de client pour un projet.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "project_testimonials")
|
||||||
|
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@ToString
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class Testimonial implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@NotBlank(message = "L'identifiant du témoignage est obligatoire")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@NotBlank(message = "Le nom du client est obligatoire")
|
||||||
|
@Size(min = 3, max = 100)
|
||||||
|
private String clientName;
|
||||||
|
|
||||||
|
@NotBlank(message = "Le poste du client est obligatoire")
|
||||||
|
@Size(min = 3, max = 100)
|
||||||
|
private String clientPosition;
|
||||||
|
|
||||||
|
@NotBlank(message = "Le contenu du témoignage est obligatoire")
|
||||||
|
@Size(min = 10, max = 1000)
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Size(max = 255)
|
||||||
|
private String clientImage;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "project_id", nullable = false)
|
||||||
|
private Project project;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private int rating = 5;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private boolean isFeatured = false;
|
||||||
|
|
||||||
|
@Column(name = "completion_date")
|
||||||
|
private LocalDateTime completionDate;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructeur par défaut requis pour JPA.
|
||||||
|
*/
|
||||||
|
public Testimonial() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle instance de témoignage.
|
||||||
|
*/
|
||||||
|
public static Testimonial create(String clientName, String clientPosition, String content,
|
||||||
|
String clientImage, Project project, int rating, boolean isFeatured) {
|
||||||
|
Testimonial testimonial = new Testimonial();
|
||||||
|
testimonial.setClientName(clientName);
|
||||||
|
testimonial.setClientPosition(clientPosition);
|
||||||
|
testimonial.setContent(content);
|
||||||
|
testimonial.setClientImage(clientImage);
|
||||||
|
testimonial.setProject(project);
|
||||||
|
testimonial.setRating(rating);
|
||||||
|
testimonial.setFeatured(isFeatured);
|
||||||
|
testimonial.setCreatedAt(LocalDateTime.now());
|
||||||
|
return testimonial;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder personnalisé pour permettre d'ajouter un titre de projet.
|
||||||
|
*/
|
||||||
|
public static class TestimonialBuilder {
|
||||||
|
public TestimonialBuilder projectTitle(String title) {
|
||||||
|
if (this.project == null) {
|
||||||
|
this.project = new Project();
|
||||||
|
}
|
||||||
|
this.project.setTitle(title);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestimonialBuilder date(LocalDateTime completionDate) {
|
||||||
|
this.completionDate = completionDate;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean equals(Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Class<?> oEffectiveClass =
|
||||||
|
o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer()
|
||||||
|
.getPersistentClass() : o.getClass();
|
||||||
|
Class<?> thisEffectiveClass =
|
||||||
|
this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer()
|
||||||
|
.getPersistentClass()
|
||||||
|
: this.getClass();
|
||||||
|
if (thisEffectiveClass != oEffectiveClass) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Testimonial that = (Testimonial) o;
|
||||||
|
return getId() != null && Objects.equals(getId(), that.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final int hashCode() {
|
||||||
|
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer()
|
||||||
|
.getPersistentClass().hashCode()
|
||||||
|
: getClass().hashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
264
src/main/java/dev/lions/repositories/AnalyticsRepository.java
Normal file
264
src/main/java/dev/lions/repositories/AnalyticsRepository.java
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
package dev.lions.repositories;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
import jakarta.persistence.TypedQuery;
|
||||||
|
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||||
|
import jakarta.persistence.criteria.CriteriaQuery;
|
||||||
|
import jakarta.persistence.criteria.Root;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import dev.lions.events.AnalyticsEvent;
|
||||||
|
import dev.lions.exceptions.RepositoryException;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository gérant la persistance des événements analytiques.
|
||||||
|
* Cette classe assure le stockage, la récupération et l'analyse
|
||||||
|
* des événements analytiques de l'application.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 1.1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class AnalyticsRepository extends BaseRepository<AnalyticsEvent, Long> {
|
||||||
|
|
||||||
|
@PersistenceContext
|
||||||
|
private EntityManager entityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche tous les événements pour une période donnée.
|
||||||
|
*
|
||||||
|
* @param startDate Date de début de la période
|
||||||
|
* @param endDate Date de fin de la période
|
||||||
|
* @return Liste des événements trouvés
|
||||||
|
* @throws RepositoryException En cas d'erreur de requête
|
||||||
|
*/
|
||||||
|
public List<AnalyticsEvent> findEventsByDateRange(@NotNull LocalDateTime startDate,
|
||||||
|
@NotNull LocalDateTime endDate) {
|
||||||
|
log.debug("Recherche des événements entre {} et {}", startDate, endDate);
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<AnalyticsEvent> query = entityManager.createQuery(
|
||||||
|
"SELECT e FROM AnalyticsEvent e " +
|
||||||
|
"WHERE e.timestamp BETWEEN :startDate AND :endDate " +
|
||||||
|
"ORDER BY e.timestamp DESC",
|
||||||
|
AnalyticsEvent.class);
|
||||||
|
|
||||||
|
query.setParameter("startDate", startDate);
|
||||||
|
query.setParameter("endDate", endDate);
|
||||||
|
|
||||||
|
List<AnalyticsEvent> events = query.getResultList();
|
||||||
|
log.info("Trouvé {} événements pour la période demandée", events.size());
|
||||||
|
|
||||||
|
return events;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la recherche des événements par période", e);
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche des événements par période", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche les événements analytiques par type pour une période donnée.
|
||||||
|
*
|
||||||
|
* @param eventType Type d'événement recherché
|
||||||
|
* @param startDate Date de début
|
||||||
|
* @param endDate Date de fin
|
||||||
|
* @return Liste des événements trouvés
|
||||||
|
*/
|
||||||
|
public List<AnalyticsEvent> findEventsByType(@NotNull String eventType,
|
||||||
|
@NotNull LocalDateTime startDate,
|
||||||
|
@NotNull LocalDateTime endDate) {
|
||||||
|
log.debug("Recherche des événements de type {} entre {} et {}",
|
||||||
|
eventType, startDate, endDate);
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<AnalyticsEvent> query = entityManager.createQuery(
|
||||||
|
"SELECT e FROM AnalyticsEvent e " +
|
||||||
|
"WHERE e.eventType = :eventType " +
|
||||||
|
"AND e.timestamp BETWEEN :startDate AND :endDate " +
|
||||||
|
"ORDER BY e.timestamp DESC",
|
||||||
|
AnalyticsEvent.class);
|
||||||
|
|
||||||
|
query.setParameter("eventType", eventType);
|
||||||
|
query.setParameter("startDate", startDate);
|
||||||
|
query.setParameter("endDate", endDate);
|
||||||
|
|
||||||
|
return query.getResultList();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche des événements par type", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistre un nouvel événement analytique.
|
||||||
|
*
|
||||||
|
* @param event Événement à sauvegarder
|
||||||
|
* @return Événement sauvegardé
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public AnalyticsEvent save(AnalyticsEvent event) {
|
||||||
|
log.debug("Sauvegarde d'un nouvel événement de type: {}", event.getEventType());
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (event.getId() == null) {
|
||||||
|
entityManager.persist(event);
|
||||||
|
log.info("Nouvel événement créé avec l'ID: {}", event.getId());
|
||||||
|
} else {
|
||||||
|
event = entityManager.merge(event);
|
||||||
|
log.info("Événement mis à jour avec l'ID: {}", event.getId());
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la sauvegarde de l'événement", e);
|
||||||
|
throw new RepositoryException("Erreur lors de la sauvegarde de l'événement", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule le nombre d'événements par type pour une période donnée.
|
||||||
|
*
|
||||||
|
* @param startDate Date de début
|
||||||
|
* @param endDate Date de fin
|
||||||
|
* @return Map des compteurs par type d'événement
|
||||||
|
*/
|
||||||
|
public Map<String, Long> getEventCountByType(@NotNull LocalDateTime startDate,
|
||||||
|
@NotNull LocalDateTime endDate) {
|
||||||
|
log.debug("Calcul du nombre d'événements entre {} et {}", startDate, endDate);
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<Object[]> results = entityManager.createQuery(
|
||||||
|
"SELECT e.eventType, COUNT(e) FROM AnalyticsEvent e " +
|
||||||
|
"WHERE e.timestamp BETWEEN :startDate AND :endDate " +
|
||||||
|
"GROUP BY e.eventType ORDER BY COUNT(e) DESC",
|
||||||
|
Object[].class)
|
||||||
|
.setParameter("startDate", startDate)
|
||||||
|
.setParameter("endDate", endDate)
|
||||||
|
.getResultList();
|
||||||
|
|
||||||
|
return results.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
row -> (String) row[0],
|
||||||
|
row -> (Long) row[1]
|
||||||
|
));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors du calcul du nombre d'événements", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche les événements associés à un contact spécifique.
|
||||||
|
*
|
||||||
|
* @param contactId Identifiant du contact
|
||||||
|
* @return Liste des événements associés
|
||||||
|
*/
|
||||||
|
public List<AnalyticsEvent> findEventsByContactId(@NotNull String contactId) {
|
||||||
|
log.debug("Recherche des événements pour le contact {}", contactId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<AnalyticsEvent> query = entityManager.createQuery(
|
||||||
|
"SELECT e FROM AnalyticsEvent e " +
|
||||||
|
"WHERE e.contactId = :contactId " +
|
||||||
|
"ORDER BY e.timestamp DESC",
|
||||||
|
AnalyticsEvent.class);
|
||||||
|
|
||||||
|
query.setParameter("contactId", contactId);
|
||||||
|
return query.getResultList();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche des événements par contact", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime les événements antérieurs à une date donnée.
|
||||||
|
*
|
||||||
|
* @param retentionDate Date limite de conservation
|
||||||
|
* @return Nombre d'événements supprimés
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public int deleteEventsOlderThan(@NotNull LocalDateTime retentionDate) {
|
||||||
|
log.info("Suppression des événements antérieurs à {}", retentionDate);
|
||||||
|
|
||||||
|
try {
|
||||||
|
int deletedCount = entityManager.createQuery(
|
||||||
|
"DELETE FROM AnalyticsEvent e WHERE e.timestamp < :retentionDate")
|
||||||
|
.setParameter("retentionDate", retentionDate)
|
||||||
|
.executeUpdate();
|
||||||
|
|
||||||
|
log.info("{} événements supprimés", deletedCount);
|
||||||
|
return deletedCount;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la suppression des anciens événements", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche un événement par son identifiant.
|
||||||
|
*
|
||||||
|
* @param id Identifiant de l'événement
|
||||||
|
* @return Optional contenant l'événement s'il existe
|
||||||
|
*/
|
||||||
|
public Optional<AnalyticsEvent> findById(Long id) {
|
||||||
|
log.debug("Recherche de l'événement avec l'ID: {}", id);
|
||||||
|
try {
|
||||||
|
AnalyticsEvent event = entityManager.find(AnalyticsEvent.class, id);
|
||||||
|
return Optional.ofNullable(event);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche de l'événement par ID", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule les statistiques d'événements par environnement.
|
||||||
|
*
|
||||||
|
* @param startDate Date de début
|
||||||
|
* @param endDate Date de fin
|
||||||
|
* @return Map des statistiques par environnement
|
||||||
|
*/
|
||||||
|
public Map<String, Long> getEventStatistics(LocalDateTime startDate,
|
||||||
|
LocalDateTime endDate) {
|
||||||
|
log.debug("Calcul des statistiques entre {} et {}", startDate, endDate);
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<Object[]> results = entityManager.createQuery(
|
||||||
|
"SELECT e.environment, COUNT(e) FROM AnalyticsEvent e " +
|
||||||
|
"WHERE e.timestamp BETWEEN :startDate AND :endDate " +
|
||||||
|
"GROUP BY e.environment",
|
||||||
|
Object[].class)
|
||||||
|
.setParameter("startDate", startDate)
|
||||||
|
.setParameter("endDate", endDate)
|
||||||
|
.getResultList();
|
||||||
|
|
||||||
|
return results.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
row -> (String) row[0],
|
||||||
|
row -> (Long) row[1]
|
||||||
|
));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors du calcul des statistiques", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
194
src/main/java/dev/lions/repositories/BaseRepository.java
Normal file
194
src/main/java/dev/lions/repositories/BaseRepository.java
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package dev.lions.repositories;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
import jakarta.persistence.TypedQuery;
|
||||||
|
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||||
|
import jakarta.persistence.criteria.CriteriaQuery;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import dev.lions.exceptions.RepositoryException;
|
||||||
|
|
||||||
|
import java.lang.reflect.ParameterizedType;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository générique fournissant les opérations CRUD de base.
|
||||||
|
* Cette classe abstract implémente les fonctionnalités communes à tous les repositories
|
||||||
|
* de l'application en assurant une gestion cohérente des entités.
|
||||||
|
*
|
||||||
|
* @param <T> Type de l'entité
|
||||||
|
* @param <ID> Type de l'identifiant de l'entité
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public abstract class BaseRepository<T, ID> {
|
||||||
|
|
||||||
|
@PersistenceContext
|
||||||
|
protected EntityManager entityManager;
|
||||||
|
|
||||||
|
private final Class<T> entityClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructeur initialisant la classe d'entité via réflexion.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public BaseRepository() {
|
||||||
|
Class<?> currentClass = getClass();
|
||||||
|
while (!(currentClass.getGenericSuperclass() instanceof ParameterizedType)) {
|
||||||
|
currentClass = currentClass.getSuperclass();
|
||||||
|
}
|
||||||
|
this.entityClass = (Class<T>) ((ParameterizedType) currentClass.getGenericSuperclass())
|
||||||
|
.getActualTypeArguments()[0];
|
||||||
|
|
||||||
|
log.debug("Repository initialisé pour l'entité : {}", entityClass.getSimpleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persiste une nouvelle entité.
|
||||||
|
*
|
||||||
|
* @param entity Entité à persister
|
||||||
|
* @return Entité persistée
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public T save(T entity) {
|
||||||
|
try {
|
||||||
|
log.debug("Sauvegarde d'une nouvelle entité : {}", entityClass.getSimpleName());
|
||||||
|
entityManager.persist(entity);
|
||||||
|
entityManager.flush();
|
||||||
|
log.info("Entité sauvegardée avec succès : {}", entity);
|
||||||
|
return entity;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException("Erreur lors de la sauvegarde de l'entité", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour une entité existante.
|
||||||
|
*
|
||||||
|
* @param entity Entité à mettre à jour
|
||||||
|
* @return Entité mise à jour
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public T update(T entity) {
|
||||||
|
try {
|
||||||
|
log.debug("Mise à jour d'une entité : {}", entityClass.getSimpleName());
|
||||||
|
T updatedEntity = entityManager.merge(entity);
|
||||||
|
entityManager.flush();
|
||||||
|
log.info("Entité mise à jour avec succès : {}", entity);
|
||||||
|
return updatedEntity;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException("Erreur lors de la mise à jour de l'entité", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche une entité par son identifiant.
|
||||||
|
*
|
||||||
|
* @param id Identifiant de l'entité
|
||||||
|
* @return Entité trouvée (Optional)
|
||||||
|
*/
|
||||||
|
public Optional<T> findById(ID id) {
|
||||||
|
try {
|
||||||
|
log.debug("Recherche de l'entité {} avec l'id : {}",
|
||||||
|
entityClass.getSimpleName(), id);
|
||||||
|
return Optional.ofNullable(entityManager.find(entityClass, id));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException("Erreur lors de la recherche de l'entité", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les entités.
|
||||||
|
*
|
||||||
|
* @return Liste des entités
|
||||||
|
*/
|
||||||
|
public List<T> findAll() {
|
||||||
|
try {
|
||||||
|
log.debug("Récupération de toutes les entités : {}",
|
||||||
|
entityClass.getSimpleName());
|
||||||
|
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||||
|
CriteriaQuery<T> cq = cb.createQuery(entityClass);
|
||||||
|
cq.from(entityClass);
|
||||||
|
TypedQuery<T> query = entityManager.createQuery(cq);
|
||||||
|
return query.getResultList();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException("Erreur lors de la récupération des entités", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une entité.
|
||||||
|
*
|
||||||
|
* @param entity Entité à supprimer
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void delete(T entity) {
|
||||||
|
try {
|
||||||
|
log.debug("Suppression de l'entité : {}", entity);
|
||||||
|
if (!entityManager.contains(entity)) {
|
||||||
|
entity = entityManager.merge(entity);
|
||||||
|
}
|
||||||
|
entityManager.remove(entity);
|
||||||
|
entityManager.flush();
|
||||||
|
log.info("Entité supprimée avec succès : {}", entity);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException("Erreur lors de la suppression de l'entité", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une entité par son identifiant.
|
||||||
|
*
|
||||||
|
* @param id Identifiant de l'entité à supprimer
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void deleteById(ID id) {
|
||||||
|
try {
|
||||||
|
log.debug("Suppression de l'entité {} avec l'id : {}",
|
||||||
|
entityClass.getSimpleName(), id);
|
||||||
|
findById(id).ifPresent(this::delete);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException("Erreur lors de la suppression de l'entité", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie l'existence d'une entité par son identifiant.
|
||||||
|
*
|
||||||
|
* @param id Identifiant à vérifier
|
||||||
|
* @return true si l'entité existe
|
||||||
|
*/
|
||||||
|
public boolean existsById(ID id) {
|
||||||
|
try {
|
||||||
|
log.debug("Vérification de l'existence de l'entité {} avec l'id : {}",
|
||||||
|
entityClass.getSimpleName(), id);
|
||||||
|
return findById(id).isPresent();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la vérification de l'existence de l'entité", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compte le nombre total d'entités.
|
||||||
|
*
|
||||||
|
* @return Nombre total d'entités
|
||||||
|
*/
|
||||||
|
public long count() {
|
||||||
|
try {
|
||||||
|
log.debug("Comptage des entités : {}", entityClass.getSimpleName());
|
||||||
|
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||||
|
CriteriaQuery<Long> cq = cb.createQuery(Long.class);
|
||||||
|
cq.select(cb.count(cq.from(entityClass)));
|
||||||
|
return entityManager.createQuery(cq).getSingleResult();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors du comptage des entités", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
188
src/main/java/dev/lions/repositories/ContactRepository.java
Normal file
188
src/main/java/dev/lions/repositories/ContactRepository.java
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package dev.lions.repositories;
|
||||||
|
|
||||||
|
import dev.lions.models.Contact;
|
||||||
|
import dev.lions.models.ContactStatus;
|
||||||
|
import dev.lions.exceptions.RepositoryException;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
import jakarta.persistence.TypedQuery;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository gérant la persistance des contacts dans l'application.
|
||||||
|
* Cette classe assure le stockage, la récupération et la gestion des contacts
|
||||||
|
* en implémentant des fonctionnalités spécifiques au traitement des demandes
|
||||||
|
* de contact.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ContactRepository extends BaseRepository<Contact, Long> {
|
||||||
|
|
||||||
|
@PersistenceContext
|
||||||
|
private EntityManager entityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche les contacts par statut avec tri par date de soumission.
|
||||||
|
* Cette méthode permet de filtrer les contacts selon leur état de traitement.
|
||||||
|
*
|
||||||
|
* @param status Statut des contacts à rechercher
|
||||||
|
* @return Liste des contacts correspondant au statut
|
||||||
|
*/
|
||||||
|
public List<Contact> findByStatus(@NotNull ContactStatus status) {
|
||||||
|
log.debug("Recherche des contacts avec le statut : {}", status);
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<Contact> query = entityManager.createQuery(
|
||||||
|
"SELECT c FROM Contact c " +
|
||||||
|
"WHERE c.status = :status " +
|
||||||
|
"ORDER BY c.submitDate DESC",
|
||||||
|
Contact.class
|
||||||
|
);
|
||||||
|
|
||||||
|
query.setParameter("status", status);
|
||||||
|
return query.getResultList();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche des contacts par statut", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les contacts non traités pour suivi.
|
||||||
|
* Cette méthode retourne les contacts qui nécessitent une attention,
|
||||||
|
* soit nouveaux soit en cours de traitement.
|
||||||
|
*
|
||||||
|
* @return Liste des contacts à traiter
|
||||||
|
*/
|
||||||
|
public List<Contact> findUnprocessedContacts() {
|
||||||
|
log.debug("Recherche des contacts non traités");
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<Contact> query = entityManager.createQuery(
|
||||||
|
"SELECT c FROM Contact c " +
|
||||||
|
"WHERE c.status IN (:statuses) " +
|
||||||
|
"ORDER BY c.submitDate ASC",
|
||||||
|
Contact.class
|
||||||
|
);
|
||||||
|
|
||||||
|
query.setParameter("statuses",
|
||||||
|
List.of(ContactStatus.NEW, ContactStatus.IN_PROGRESS));
|
||||||
|
return query.getResultList();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche des contacts non traités", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche les contacts soumis dans une période donnée.
|
||||||
|
*
|
||||||
|
* @param startDate Date de début de la période
|
||||||
|
* @param endDate Date de fin de la période
|
||||||
|
* @return Liste des contacts pour la période
|
||||||
|
*/
|
||||||
|
public List<Contact> findBySubmitDateBetween(
|
||||||
|
@NotNull LocalDateTime startDate,
|
||||||
|
@NotNull LocalDateTime endDate) {
|
||||||
|
|
||||||
|
log.debug("Recherche des contacts entre {} et {}", startDate, endDate);
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<Contact> query = entityManager.createQuery(
|
||||||
|
"SELECT c FROM Contact c " +
|
||||||
|
"WHERE c.submitDate BETWEEN :startDate AND :endDate " +
|
||||||
|
"ORDER BY c.submitDate DESC",
|
||||||
|
Contact.class
|
||||||
|
);
|
||||||
|
|
||||||
|
query.setParameter("startDate", startDate);
|
||||||
|
query.setParameter("endDate", endDate);
|
||||||
|
return query.getResultList();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche des contacts par période", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le statut d'un contact.
|
||||||
|
*
|
||||||
|
* @param contactId Identifiant du contact
|
||||||
|
* @param newStatus Nouveau statut
|
||||||
|
* @param processDate Date de traitement
|
||||||
|
* @return Contact mis à jour
|
||||||
|
*/
|
||||||
|
public Optional<Contact> updateStatus(
|
||||||
|
@NotNull Long contactId,
|
||||||
|
@NotNull ContactStatus newStatus,
|
||||||
|
LocalDateTime processDate) {
|
||||||
|
|
||||||
|
log.debug("Mise à jour du statut du contact {} vers {}", contactId, newStatus);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Contact contact = entityManager.find(Contact.class, contactId);
|
||||||
|
if (contact == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
contact.setStatus(newStatus);
|
||||||
|
if (processDate != null) {
|
||||||
|
contact.setProcessDate(processDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
Contact updatedContact = update(contact);
|
||||||
|
log.info("Statut du contact {} mis à jour vers {}", contactId, newStatus);
|
||||||
|
return Optional.of(updatedContact);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la mise à jour du statut du contact", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une note interne à un contact.
|
||||||
|
*
|
||||||
|
* @param contactId Identifiant du contact
|
||||||
|
* @param note Note à ajouter
|
||||||
|
* @return Contact mis à jour
|
||||||
|
*/
|
||||||
|
public Optional<Contact> addInternalNote(
|
||||||
|
@NotNull Long contactId,
|
||||||
|
@NotNull String note) {
|
||||||
|
|
||||||
|
log.debug("Ajout d'une note au contact {}", contactId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Contact contact = entityManager.find(Contact.class, contactId);
|
||||||
|
if (contact == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentNotes = contact.getInternalNotes();
|
||||||
|
String updatedNotes = currentNotes == null ? note :
|
||||||
|
currentNotes + "\n" + LocalDateTime.now() + ": " + note;
|
||||||
|
|
||||||
|
contact.setInternalNotes(updatedNotes);
|
||||||
|
Contact updatedContact = update(contact);
|
||||||
|
|
||||||
|
log.info("Note ajoutée au contact {}", contactId);
|
||||||
|
return Optional.of(updatedContact);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de l'ajout de la note au contact", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package dev.lions.repositories;
|
||||||
|
|
||||||
|
import dev.lions.models.EmailTemplate;
|
||||||
|
import dev.lions.exceptions.RepositoryException;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.NoResultException;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
import jakarta.persistence.TypedQuery;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository gérant la persistance des modèles d'emails de l'application.
|
||||||
|
* Cette classe assure le stockage, la récupération et la gestion des templates
|
||||||
|
* d'emails avec support multilingue et versionnement.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class EmailTemplateRepository extends BaseRepository<EmailTemplate, Long> {
|
||||||
|
|
||||||
|
@PersistenceContext
|
||||||
|
private EntityManager entityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche un modèle d'email par son nom.
|
||||||
|
* Cette méthode récupère la dernière version active du modèle.
|
||||||
|
*
|
||||||
|
* @param templateName Nom du modèle recherché
|
||||||
|
* @return Modèle trouvé (Optional)
|
||||||
|
*/
|
||||||
|
public Optional<EmailTemplate> findByName(@NotBlank String templateName) {
|
||||||
|
log.debug("Recherche du modèle d'email : {}", templateName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<EmailTemplate> query = entityManager.createQuery(
|
||||||
|
"SELECT t FROM EmailTemplate t " +
|
||||||
|
"WHERE t.templateName = :name " +
|
||||||
|
"AND t.active = true " +
|
||||||
|
"ORDER BY t.version DESC",
|
||||||
|
EmailTemplate.class
|
||||||
|
);
|
||||||
|
|
||||||
|
query.setParameter("name", templateName);
|
||||||
|
query.setMaxResults(1);
|
||||||
|
|
||||||
|
return Optional.of(query.getSingleResult());
|
||||||
|
|
||||||
|
} catch (NoResultException e) {
|
||||||
|
log.debug("Aucun modèle trouvé pour le nom : {}", templateName);
|
||||||
|
return Optional.empty();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche du modèle d'email", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche un modèle d'email par son nom et sa locale.
|
||||||
|
* Permet de récupérer des modèles localisés spécifiques.
|
||||||
|
*
|
||||||
|
* @param templateName Nom du modèle
|
||||||
|
* @param locale Code de la langue
|
||||||
|
* @return Modèle trouvé (Optional)
|
||||||
|
*/
|
||||||
|
public Optional<EmailTemplate> findByNameAndLocale(
|
||||||
|
@NotBlank String templateName,
|
||||||
|
@NotBlank String locale) {
|
||||||
|
|
||||||
|
log.debug("Recherche du modèle d'email : {} pour la locale : {}",
|
||||||
|
templateName, locale);
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<EmailTemplate> query = entityManager.createQuery(
|
||||||
|
"SELECT t FROM EmailTemplate t " +
|
||||||
|
"WHERE t.templateName = :name " +
|
||||||
|
"AND t.locale = :locale " +
|
||||||
|
"AND t.active = true " +
|
||||||
|
"ORDER BY t.version DESC",
|
||||||
|
EmailTemplate.class
|
||||||
|
);
|
||||||
|
|
||||||
|
query.setParameter("name", templateName);
|
||||||
|
query.setParameter("locale", locale);
|
||||||
|
query.setMaxResults(1);
|
||||||
|
|
||||||
|
return Optional.of(query.getSingleResult());
|
||||||
|
|
||||||
|
} catch (NoResultException e) {
|
||||||
|
log.debug("Aucun modèle trouvé pour le nom : {} et la locale : {}",
|
||||||
|
templateName, locale);
|
||||||
|
return Optional.empty();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche du modèle d'email localisé", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée ou met à jour un modèle d'email.
|
||||||
|
* Gère automatiquement le versionnement des modèles.
|
||||||
|
*
|
||||||
|
* @param template Modèle à sauvegarder
|
||||||
|
* @return Modèle sauvegardé
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public EmailTemplate save(EmailTemplate template) {
|
||||||
|
log.debug("Sauvegarde du modèle d'email : {}", template.getTemplateName());
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (template.getId() == null) {
|
||||||
|
setNextVersion(template);
|
||||||
|
entityManager.persist(template);
|
||||||
|
} else {
|
||||||
|
template = entityManager.merge(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
entityManager.flush();
|
||||||
|
log.info("Modèle d'email sauvegardé avec succès : {}",
|
||||||
|
template.getTemplateName());
|
||||||
|
|
||||||
|
return template;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la sauvegarde du modèle d'email", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Définit la prochaine version pour un nouveau modèle.
|
||||||
|
*/
|
||||||
|
private void setNextVersion(EmailTemplate template) {
|
||||||
|
try {
|
||||||
|
TypedQuery<Long> query = entityManager.createQuery(
|
||||||
|
"SELECT MAX(t.version) FROM EmailTemplate t " +
|
||||||
|
"WHERE t.templateName = :name",
|
||||||
|
Long.class
|
||||||
|
);
|
||||||
|
|
||||||
|
query.setParameter("name", template.getTemplateName());
|
||||||
|
Long maxVersion = query.getSingleResult();
|
||||||
|
|
||||||
|
template.setVersion(maxVersion == null ? 1L : maxVersion + 1);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la définition de la version du modèle", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime tous les modèles d'un certain nom.
|
||||||
|
*
|
||||||
|
* @param templateName Nom des modèles à supprimer
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void deleteByName(@NotBlank String templateName) {
|
||||||
|
log.debug("Suppression des modèles d'email : {}", templateName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
int deletedCount = entityManager.createQuery(
|
||||||
|
"DELETE FROM EmailTemplate t WHERE t.templateName = :name")
|
||||||
|
.setParameter("name", templateName)
|
||||||
|
.executeUpdate();
|
||||||
|
|
||||||
|
log.info("{} modèles d'email supprimés pour le nom : {}",
|
||||||
|
deletedCount, templateName);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la suppression des modèles d'email", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie l'existence d'un modèle par son nom.
|
||||||
|
*
|
||||||
|
* @param templateName Nom du modèle à vérifier
|
||||||
|
* @return true si le modèle existe
|
||||||
|
*/
|
||||||
|
public boolean existsByName(@NotBlank String templateName) {
|
||||||
|
log.debug("Vérification de l'existence du modèle : {}", templateName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Long count = entityManager.createQuery(
|
||||||
|
"SELECT COUNT(t) FROM EmailTemplate t WHERE t.templateName = :name",
|
||||||
|
Long.class)
|
||||||
|
.setParameter("name", templateName)
|
||||||
|
.getSingleResult();
|
||||||
|
|
||||||
|
return count > 0;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la vérification de l'existence du modèle", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
238
src/main/java/dev/lions/repositories/NotificationRepository.java
Normal file
238
src/main/java/dev/lions/repositories/NotificationRepository.java
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
package dev.lions.repositories;
|
||||||
|
|
||||||
|
import dev.lions.models.Notification;
|
||||||
|
import dev.lions.models.NotificationStatus;
|
||||||
|
import dev.lions.models.NotificationType;
|
||||||
|
import dev.lions.exceptions.RepositoryException;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.NoResultException;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
import jakarta.persistence.TypedQuery;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository gérant la persistance des notifications système.
|
||||||
|
* Cette classe assure le stockage et la récupération des notifications
|
||||||
|
* avec support pour le filtrage par statut, type et période.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class NotificationRepository extends BaseRepository<Notification, Long> {
|
||||||
|
|
||||||
|
@PersistenceContext
|
||||||
|
private EntityManager entityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les notifications non lues.
|
||||||
|
* Cette méthode retourne les notifications qui nécessitent
|
||||||
|
* l'attention des utilisateurs.
|
||||||
|
*
|
||||||
|
* @return Liste des notifications non lues
|
||||||
|
*/
|
||||||
|
public List<Notification> findUnreadNotifications() {
|
||||||
|
log.debug("Recherche des notifications non lues");
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<Notification> query = entityManager.createQuery(
|
||||||
|
"SELECT n FROM Notification n " +
|
||||||
|
"WHERE n.status = :status " +
|
||||||
|
"ORDER BY n.timestamp DESC",
|
||||||
|
Notification.class
|
||||||
|
);
|
||||||
|
|
||||||
|
query.setParameter("status", NotificationStatus.UNREAD);
|
||||||
|
return query.getResultList();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche des notifications non lues", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche les notifications pour une période donnée.
|
||||||
|
*
|
||||||
|
* @param start Date de début
|
||||||
|
* @param end Date de fin
|
||||||
|
* @return Liste des notifications pour la période
|
||||||
|
*/
|
||||||
|
public List<Notification> findNotificationsByDateRange(
|
||||||
|
@NotNull LocalDateTime start,
|
||||||
|
@NotNull LocalDateTime end) {
|
||||||
|
|
||||||
|
log.debug("Recherche des notifications entre {} et {}", start, end);
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<Notification> query = entityManager.createQuery(
|
||||||
|
"SELECT n FROM Notification n " +
|
||||||
|
"WHERE n.timestamp BETWEEN :start AND :end " +
|
||||||
|
"ORDER BY n.timestamp DESC",
|
||||||
|
Notification.class
|
||||||
|
);
|
||||||
|
|
||||||
|
query.setParameter("start", start);
|
||||||
|
query.setParameter("end", end);
|
||||||
|
return query.getResultList();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche des notifications par période", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les notifications critiques non lues.
|
||||||
|
* Ces notifications représentent des alertes importantes nécessitant
|
||||||
|
* une attention immédiate.
|
||||||
|
*
|
||||||
|
* @return Liste des notifications critiques
|
||||||
|
*/
|
||||||
|
public List<Notification> findCriticalNotifications() {
|
||||||
|
log.debug("Recherche des notifications critiques");
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<Notification> query = entityManager.createQuery(
|
||||||
|
"SELECT n FROM Notification n " +
|
||||||
|
"WHERE n.type.isCritical = true " +
|
||||||
|
"AND n.status = :status " +
|
||||||
|
"ORDER BY n.timestamp DESC",
|
||||||
|
Notification.class
|
||||||
|
);
|
||||||
|
|
||||||
|
query.setParameter("status", NotificationStatus.UNREAD);
|
||||||
|
return query.getResultList();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche des notifications critiques", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marque une notification comme lue.
|
||||||
|
*
|
||||||
|
* @param notificationId Identifiant de la notification
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void markAsRead(@NotNull Long notificationId) {
|
||||||
|
log.debug("Marquage de la notification {} comme lue", notificationId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Notification notification = findById(notificationId)
|
||||||
|
.orElseThrow(() -> new NoResultException("Notification non trouvée"));
|
||||||
|
|
||||||
|
notification.setStatus(NotificationStatus.READ);
|
||||||
|
notification.setReadTimestamp(LocalDateTime.now());
|
||||||
|
update(notification);
|
||||||
|
|
||||||
|
log.info("Notification {} marquée comme lue", notificationId);
|
||||||
|
|
||||||
|
} catch (NoResultException e) {
|
||||||
|
log.warn("Tentative de marquage d'une notification inexistante : {}",
|
||||||
|
notificationId);
|
||||||
|
throw new RepositoryException("Notification non trouvée", e);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors du marquage de la notification comme lue", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marque toutes les notifications non lues comme lues.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void markAllAsRead() {
|
||||||
|
log.debug("Marquage de toutes les notifications comme lues");
|
||||||
|
|
||||||
|
try {
|
||||||
|
int updatedCount = entityManager.createQuery(
|
||||||
|
"UPDATE Notification n " +
|
||||||
|
"SET n.status = :newStatus, n.readTimestamp = :timestamp " +
|
||||||
|
"WHERE n.status = :oldStatus"
|
||||||
|
)
|
||||||
|
.setParameter("newStatus", NotificationStatus.READ)
|
||||||
|
.setParameter("timestamp", LocalDateTime.now())
|
||||||
|
.setParameter("oldStatus", NotificationStatus.UNREAD)
|
||||||
|
.executeUpdate();
|
||||||
|
|
||||||
|
log.info("{} notifications marquées comme lues", updatedCount);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors du marquage de toutes les notifications comme lues", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compte les notifications par type pour une période donnée.
|
||||||
|
*
|
||||||
|
* @param start Date de début
|
||||||
|
* @param end Date de fin
|
||||||
|
* @return Nombre de notifications par type
|
||||||
|
*/
|
||||||
|
public Map<NotificationType, Long> countByType(
|
||||||
|
@NotNull LocalDateTime start,
|
||||||
|
@NotNull LocalDateTime end) {
|
||||||
|
|
||||||
|
log.debug("Comptage des notifications par type entre {} et {}", start, end);
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<Object[]> results = entityManager.createQuery(
|
||||||
|
"SELECT n.type, COUNT(n) FROM Notification n " +
|
||||||
|
"WHERE n.timestamp BETWEEN :start AND :end " +
|
||||||
|
"GROUP BY n.type",
|
||||||
|
Object[].class
|
||||||
|
)
|
||||||
|
.setParameter("start", start)
|
||||||
|
.setParameter("end", end)
|
||||||
|
.getResultList();
|
||||||
|
|
||||||
|
return results.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
row -> (NotificationType) row[0],
|
||||||
|
row -> (Long) row[1]
|
||||||
|
));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors du comptage des notifications par type", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime les notifications antérieures à une date donnée.
|
||||||
|
*
|
||||||
|
* @param retentionDate Date de conservation des notifications
|
||||||
|
* @return Nombre de notifications supprimées
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public int deleteEventsOlderThan(@NotNull LocalDateTime retentionDate) {
|
||||||
|
log.info("Suppression des notifications antérieures à {}", retentionDate);
|
||||||
|
|
||||||
|
try {
|
||||||
|
int deletedCount = entityManager.createQuery(
|
||||||
|
"DELETE FROM Notification e WHERE e.timestamp < :retentionDate")
|
||||||
|
.setParameter("retentionDate", retentionDate)
|
||||||
|
.executeUpdate();
|
||||||
|
|
||||||
|
log.info("{} notifications supprimées", deletedCount);
|
||||||
|
return deletedCount;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la suppression des anciennes notifications", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
228
src/main/java/dev/lions/repositories/ProjectRepository.java
Normal file
228
src/main/java/dev/lions/repositories/ProjectRepository.java
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
package dev.lions.repositories;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
import jakarta.persistence.TypedQuery;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
|
||||||
|
import dev.lions.models.Project;
|
||||||
|
import dev.lions.exceptions.RepositoryException;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository gérant la persistance des projets dans l'application.
|
||||||
|
* Cette classe assure le stockage, la recherche et la gestion des projets
|
||||||
|
* avec support pour le filtrage par tags, dates et statuts.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ProjectRepository extends BaseRepository<Project, String> {
|
||||||
|
|
||||||
|
@PersistenceContext
|
||||||
|
private EntityManager entityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche les projets par ensemble de tags.
|
||||||
|
* Cette méthode permet de filtrer les projets qui contiennent au moins
|
||||||
|
* un des tags spécifiés.
|
||||||
|
*
|
||||||
|
* @param tags Liste des tags à rechercher
|
||||||
|
* @return Liste des projets correspondants
|
||||||
|
*/
|
||||||
|
public List<Project> findByTags(@NotEmpty Set<String> tags) {
|
||||||
|
log.debug("Recherche de projets par tags : {}", tags);
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<Project> query = entityManager.createQuery(
|
||||||
|
"SELECT DISTINCT p FROM Project p " +
|
||||||
|
"JOIN p.tags t " +
|
||||||
|
"WHERE t IN :tags",
|
||||||
|
Project.class
|
||||||
|
);
|
||||||
|
|
||||||
|
query.setParameter("tags", tags);
|
||||||
|
return query.getResultList();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche des projets par tags", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche les projets mis en avant.
|
||||||
|
* Permet de récupérer les projets marqués comme "featured" pour
|
||||||
|
* l'affichage en page d'accueil.
|
||||||
|
*
|
||||||
|
* @param featured État de mise en avant recherché
|
||||||
|
* @return Liste des projets mis en avant
|
||||||
|
*/
|
||||||
|
public List<Project> findByFeatured(boolean featured) {
|
||||||
|
log.debug("Recherche des projets featured={}", featured);
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<Project> query = entityManager.createQuery(
|
||||||
|
"SELECT p FROM Project p " +
|
||||||
|
"WHERE p.featured = :featured " +
|
||||||
|
"ORDER BY p.completionDate DESC",
|
||||||
|
Project.class
|
||||||
|
);
|
||||||
|
|
||||||
|
query.setParameter("featured", featured);
|
||||||
|
return query.getResultList();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche des projets mis en avant", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche les projets complétés dans une période donnée.
|
||||||
|
*
|
||||||
|
* @param startDate Date de début
|
||||||
|
* @param endDate Date de fin
|
||||||
|
* @return Liste des projets pour la période
|
||||||
|
*/
|
||||||
|
public List<Project> findByCompletionDateBetween(
|
||||||
|
@NotNull LocalDateTime startDate,
|
||||||
|
@NotNull LocalDateTime endDate) {
|
||||||
|
|
||||||
|
log.debug("Recherche des projets complétés entre {} et {}",
|
||||||
|
startDate, endDate);
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<Project> query = entityManager.createQuery(
|
||||||
|
"SELECT p FROM Project p " +
|
||||||
|
"WHERE p.completionDate BETWEEN :startDate AND :endDate " +
|
||||||
|
"ORDER BY p.completionDate DESC",
|
||||||
|
Project.class
|
||||||
|
);
|
||||||
|
|
||||||
|
query.setParameter("startDate", startDate);
|
||||||
|
query.setParameter("endDate", endDate);
|
||||||
|
return query.getResultList();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche des projets par période", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les projets les plus récents.
|
||||||
|
*
|
||||||
|
* @param limit Nombre maximum de projets à retourner
|
||||||
|
* @return Liste limitée des projets les plus récents
|
||||||
|
*/
|
||||||
|
public List<Project> findRecentProjects(int limit) {
|
||||||
|
log.debug("Recherche des {} projets les plus récents", limit);
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<Project> query = entityManager.createQuery(
|
||||||
|
"SELECT p FROM Project p " +
|
||||||
|
"ORDER BY p.completionDate DESC",
|
||||||
|
Project.class
|
||||||
|
);
|
||||||
|
|
||||||
|
query.setMaxResults(limit);
|
||||||
|
return query.getResultList();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche des projets récents", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche les projets par technologie utilisée.
|
||||||
|
*
|
||||||
|
* @param technology Technologie recherchée
|
||||||
|
* @return Liste des projets utilisant cette technologie
|
||||||
|
*/
|
||||||
|
public List<Project> findByTechnology(@NotNull String technology) {
|
||||||
|
log.debug("Recherche des projets utilisant la technologie : {}",
|
||||||
|
technology);
|
||||||
|
|
||||||
|
try {
|
||||||
|
TypedQuery<Project> query = entityManager.createQuery(
|
||||||
|
"SELECT p FROM Project p " +
|
||||||
|
"JOIN p.technologies t " +
|
||||||
|
"WHERE LOWER(t) = LOWER(:technology)",
|
||||||
|
Project.class
|
||||||
|
);
|
||||||
|
|
||||||
|
query.setParameter("technology", technology.toLowerCase());
|
||||||
|
return query.getResultList();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la recherche des projets par technologie", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le statut "featured" d'un projet.
|
||||||
|
*
|
||||||
|
* @param projectId Identifiant du projet
|
||||||
|
* @param featured Nouveau statut featured
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void updateFeaturedStatus(@NotNull String projectId, boolean featured) {
|
||||||
|
log.debug("Mise à jour du statut featured={} pour le projet {}",
|
||||||
|
featured, projectId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Project project = findById(projectId)
|
||||||
|
.orElseThrow(() -> new RepositoryException("Projet non trouvé"));
|
||||||
|
|
||||||
|
project.setFeatured(featured);
|
||||||
|
update(project);
|
||||||
|
|
||||||
|
log.info("Statut featured mis à jour pour le projet {}", projectId);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors de la mise à jour du statut featured", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compte les projets par technologie.
|
||||||
|
*
|
||||||
|
* @return Nombre de projets par technologie
|
||||||
|
*/
|
||||||
|
public Map<String, Long> countByTechnology() {
|
||||||
|
log.debug("Comptage des projets par technologie");
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<Object[]> results = entityManager.createQuery(
|
||||||
|
"SELECT t, COUNT(p) FROM Project p " +
|
||||||
|
"JOIN p.technologies t " +
|
||||||
|
"GROUP BY t",
|
||||||
|
Object[].class
|
||||||
|
).getResultList();
|
||||||
|
|
||||||
|
return results.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
row -> (String) row[0],
|
||||||
|
row -> (Long) row[1]
|
||||||
|
));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RepositoryException(
|
||||||
|
"Erreur lors du comptage des projets par technologie", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src/main/java/dev/lions/security/SecurityFilter.java
Normal file
94
src/main/java/dev/lions/security/SecurityFilter.java
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package dev.lions.security;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.servlet.Filter;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.FilterConfig;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.ServletRequest;
|
||||||
|
import jakarta.servlet.ServletResponse;
|
||||||
|
import jakarta.servlet.annotation.WebFilter;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtre de sécurité pour l'application.
|
||||||
|
* Implémente la logique de sécurité pour toutes les requêtes entrantes.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
@WebFilter(urlPatterns = "/*")
|
||||||
|
public class SecurityFilter implements Filter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le filtre.
|
||||||
|
* Cette méthode est appelée par le conteneur lors du démarrage.
|
||||||
|
*
|
||||||
|
* @param filterConfig Configuration du filtre
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void init(FilterConfig filterConfig) {
|
||||||
|
log.info("Initialisation du filtre de sécurité");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applique la logique de filtrage sur chaque requête.
|
||||||
|
*
|
||||||
|
* @param request La requête entrante
|
||||||
|
* @param response La réponse
|
||||||
|
* @param chain La chaîne de filtres
|
||||||
|
* @throws IOException En cas d'erreur d'entrée/sortie
|
||||||
|
* @throws ServletException En cas d'erreur de servlet
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||||
|
String requestUri = httpRequest.getRequestURI();
|
||||||
|
|
||||||
|
log.debug("Traitement de la requête: {}", requestUri);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Vérification de sécurité de base
|
||||||
|
if (isSecurityCheckPassed(httpRequest)) {
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
} else {
|
||||||
|
log.warn("Accès refusé pour la requête: {}", requestUri);
|
||||||
|
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
||||||
|
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Accès refusé");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors du traitement de la requête: {}", requestUri, e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effectue les vérifications de sécurité nécessaires.
|
||||||
|
*
|
||||||
|
* @param request La requête HTTP à vérifier
|
||||||
|
* @return true si la requête passe les vérifications de sécurité
|
||||||
|
*/
|
||||||
|
private boolean isSecurityCheckPassed(HttpServletRequest request) {
|
||||||
|
// Implémentez ici votre logique de sécurité spécifique
|
||||||
|
// Par exemple : vérification des tokens, authentification, autorisations...
|
||||||
|
log.trace("Vérification de sécurité pour: {}", request.getRequestURI());
|
||||||
|
return true; // À adapter selon vos besoins de sécurité
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode appelée lors de la destruction du filtre.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
log.info("Destruction du filtre de sécurité");
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/main/java/dev/lions/security/SecurityHeadersFilter.java
Normal file
70
src/main/java/dev/lions/security/SecurityHeadersFilter.java
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package dev.lions.security;
|
||||||
|
|
||||||
|
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.FilterConfig;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.ServletRequest;
|
||||||
|
import jakarta.servlet.ServletResponse;
|
||||||
|
import jakarta.servlet.annotation.WebFilter;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtre de sécurité pour ajouter des en-têtes HTTP de sécurité.
|
||||||
|
* Ce filtre ajoute automatiquement les en-têtes de sécurité recommandés à toutes les réponses.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@WebFilter("/*")
|
||||||
|
@ApplicationScoped
|
||||||
|
@RegisterForReflection
|
||||||
|
public class SecurityHeadersFilter implements jakarta.servlet.Filter {
|
||||||
|
|
||||||
|
private static final String CSP_POLICY =
|
||||||
|
"default-src 'self'; " +
|
||||||
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com; " +
|
||||||
|
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; " +
|
||||||
|
"img-src 'self' data: https:; " +
|
||||||
|
"font-src 'self' https://cdnjs.cloudflare.com; " +
|
||||||
|
"connect-src 'self'";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(FilterConfig filterConfig) throws ServletException {
|
||||||
|
log.info("Initialisation du filtre des en-têtes de sécurité");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
log.debug("Application des en-têtes de sécurité");
|
||||||
|
|
||||||
|
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
||||||
|
|
||||||
|
// En-têtes de sécurité standards
|
||||||
|
httpResponse.setHeader("X-Content-Type-Options", "nosniff");
|
||||||
|
httpResponse.setHeader("X-Frame-Options", "SAMEORIGIN");
|
||||||
|
httpResponse.setHeader("X-XSS-Protection", "1; mode=block");
|
||||||
|
httpResponse.setHeader("Content-Security-Policy", CSP_POLICY);
|
||||||
|
httpResponse.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||||
|
httpResponse.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=()");
|
||||||
|
|
||||||
|
// Ajout des en-têtes HSTS en production
|
||||||
|
if ("production".equals(System.getProperty("quarkus.profile"))) {
|
||||||
|
httpResponse.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
log.info("Destruction du filtre des en-têtes de sécurité");
|
||||||
|
}
|
||||||
|
}
|
||||||
214
src/main/java/dev/lions/services/AnalyticsService.java
Normal file
214
src/main/java/dev/lions/services/AnalyticsService.java
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
package dev.lions.services;
|
||||||
|
|
||||||
|
import dev.lions.events.AnalyticsEvent;
|
||||||
|
import dev.lions.events.AnalyticsEventPublisher;
|
||||||
|
import dev.lions.exceptions.AnalyticsException;
|
||||||
|
import dev.lions.repositories.AnalyticsRepository;
|
||||||
|
import dev.lions.utils.MetricsCollector;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsable du traitement et de l'enregistrement des événements analytiques.
|
||||||
|
* Gère l'enrichissement des données, leur persistance et leur publication.
|
||||||
|
*
|
||||||
|
* @author Lions Dev Team
|
||||||
|
* @version 1.1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class AnalyticsService {
|
||||||
|
|
||||||
|
private static final int MAX_RETRY_ATTEMPTS = 3;
|
||||||
|
private static final long RETRY_DELAY_MS = 1000;
|
||||||
|
private static final int BATCH_SIZE = 100;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AnalyticsRepository analyticsRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AnalyticsEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private MetricsCollector metricsCollector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traite et enregistre un événement analytique.
|
||||||
|
* L'événement est enrichi avec des métadonnées contextuelles avant son traitement.
|
||||||
|
*
|
||||||
|
* @param event L'événement analytique à traiter
|
||||||
|
* @return L'événement traité et enrichi
|
||||||
|
* @throws AnalyticsException Si une erreur survient pendant le traitement
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public AnalyticsEvent processEvent(@NotNull @Valid AnalyticsEvent event) {
|
||||||
|
log.debug("Début du traitement de l'événement analytique de type: {}", event.getEventType());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Enrichissement et validation
|
||||||
|
AnalyticsEvent enrichedEvent = enrichEventData(event);
|
||||||
|
validateEvent(enrichedEvent);
|
||||||
|
|
||||||
|
// Persistance avec gestion des reprises
|
||||||
|
AnalyticsEvent savedEvent = persistEventWithRetry(enrichedEvent);
|
||||||
|
|
||||||
|
// Publication asynchrone
|
||||||
|
publishEventAsync(savedEvent);
|
||||||
|
|
||||||
|
// Collecte des métriques
|
||||||
|
metricsCollector.incrementEventCounter(event.getEventType());
|
||||||
|
|
||||||
|
log.info("Événement analytique traité avec succès - ID: {}, Type: {}",
|
||||||
|
savedEvent.getId(), savedEvent.getEventType());
|
||||||
|
|
||||||
|
return savedEvent;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors du traitement de l'événement analytique", e);
|
||||||
|
throw new AnalyticsException("Impossible de traiter l'événement analytique", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traite un lot d'événements analytiques de manière optimisée.
|
||||||
|
*
|
||||||
|
* @param events Liste des événements à traiter
|
||||||
|
* @return Liste des événements traités
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public List<AnalyticsEvent> processBatchEvents(List<AnalyticsEvent> events) {
|
||||||
|
log.debug("Traitement par lot de {} événements analytiques", events.size());
|
||||||
|
|
||||||
|
return events.stream()
|
||||||
|
.map(this::processEvent)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les événements analytiques pour une période donnée.
|
||||||
|
*
|
||||||
|
* @param startDate Date de début
|
||||||
|
* @param endDate Date de fin
|
||||||
|
* @return Liste des événements pour la période
|
||||||
|
*/
|
||||||
|
public List<dev.lions.events.AnalyticsEvent> getEventsByDateRange(LocalDateTime startDate, LocalDateTime endDate) {
|
||||||
|
log.debug("Recherche des événements entre {} et {}", startDate, endDate);
|
||||||
|
return analyticsRepository.findEventsByDateRange(startDate, endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enrichit l'événement avec des données contextuelles supplémentaires.
|
||||||
|
*
|
||||||
|
* @param event L'événement à enrichir
|
||||||
|
* @return L'événement enrichi
|
||||||
|
*/
|
||||||
|
private AnalyticsEvent enrichEventData(AnalyticsEvent event) {
|
||||||
|
log.trace("Enrichissement des données de l'événement: {}", event.getId());
|
||||||
|
|
||||||
|
Map<String, Object> contextData = Map.of(
|
||||||
|
"processTimestamp", LocalDateTime.now(),
|
||||||
|
"processingNode", System.getProperty("jboss.node.name"),
|
||||||
|
"applicationVersion", System.getProperty("app.version")
|
||||||
|
);
|
||||||
|
|
||||||
|
return event.withAdditionalProperties(contextData)
|
||||||
|
.enrichWithMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide l'intégrité et la cohérence d'un événement.
|
||||||
|
*
|
||||||
|
* @param event L'événement à valider
|
||||||
|
* @throws AnalyticsException Si l'événement est invalide
|
||||||
|
*/
|
||||||
|
private void validateEvent(AnalyticsEvent event) {
|
||||||
|
log.trace("Validation de l'événement analytique");
|
||||||
|
|
||||||
|
if (!event.isValid()) {
|
||||||
|
log.warn("Validation échouée pour l'événement: {}", event);
|
||||||
|
throw new AnalyticsException("L'événement analytique est invalide");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persiste un événement avec mécanisme de reprise en cas d'échec.
|
||||||
|
*
|
||||||
|
* @param event L'événement à persister
|
||||||
|
* @return L'événement persisté
|
||||||
|
* @throws AnalyticsException Si la persistance échoue après les reprises
|
||||||
|
*/
|
||||||
|
private AnalyticsEvent persistEventWithRetry(AnalyticsEvent event) {
|
||||||
|
Exception lastException = null;
|
||||||
|
|
||||||
|
for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
|
||||||
|
try {
|
||||||
|
return analyticsRepository.save(event);
|
||||||
|
} catch (Exception e) {
|
||||||
|
lastException = e;
|
||||||
|
log.warn("Échec de la persistance (tentative {}/{}): {}",
|
||||||
|
attempt, MAX_RETRY_ATTEMPTS, e.getMessage());
|
||||||
|
|
||||||
|
if (attempt < MAX_RETRY_ATTEMPTS) {
|
||||||
|
try {
|
||||||
|
TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS * attempt);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new AnalyticsException("Interruption pendant la reprise", ie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AnalyticsException("Échec de la persistance après " + MAX_RETRY_ATTEMPTS +
|
||||||
|
" tentatives", lastException);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publie un événement de manière asynchrone.
|
||||||
|
*
|
||||||
|
* @param event L'événement à publier
|
||||||
|
*/
|
||||||
|
private void publishEventAsync(AnalyticsEvent event) {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
eventPublisher.publish(event);
|
||||||
|
log.debug("Publication asynchrone réussie pour l'événement: {}", event.getId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la publication asynchrone de l'événement: {}",
|
||||||
|
event.getId(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie les anciens événements selon la politique de rétention.
|
||||||
|
*
|
||||||
|
* @param retentionDate Date limite de conservation
|
||||||
|
* @return Nombre d'événements supprimés
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public int cleanupOldEvents(LocalDateTime retentionDate) {
|
||||||
|
log.info("Nettoyage des événements antérieurs à {}", retentionDate);
|
||||||
|
|
||||||
|
try {
|
||||||
|
int deletedCount = analyticsRepository.deleteEventsOlderThan(retentionDate);
|
||||||
|
log.info("{} événements anciens ont été supprimés", deletedCount);
|
||||||
|
return deletedCount;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors du nettoyage des anciens événements", e);
|
||||||
|
throw new AnalyticsException("Échec du nettoyage des événements", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
229
src/main/java/dev/lions/services/ContactService.java
Normal file
229
src/main/java/dev/lions/services/ContactService.java
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
package dev.lions.services;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.event.Event;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import dev.lions.models.Contact;
|
||||||
|
import dev.lions.models.ContactForm;
|
||||||
|
import dev.lions.models.ContactStatus;
|
||||||
|
import dev.lions.models.EmailTemplate;
|
||||||
|
import dev.lions.repositories.ContactRepository;
|
||||||
|
import dev.lions.events.ContactSubmissionEvent;
|
||||||
|
import dev.lions.exceptions.BusinessException;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service gérant la logique métier des contacts.
|
||||||
|
* Cette classe assure le traitement des demandes de contact, leur validation,
|
||||||
|
* et la notification des parties concernées.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ContactService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ContactRepository contactRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private EmailService emailService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Event<ContactSubmissionEvent> contactEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traite un nouveau formulaire de contact.
|
||||||
|
* Cette méthode valide les données, enregistre le contact et envoie
|
||||||
|
* les notifications appropriées.
|
||||||
|
*
|
||||||
|
* @param form Formulaire de contact à traiter
|
||||||
|
* @return Contact créé
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Contact processContactForm(@Valid @NotNull ContactForm form) {
|
||||||
|
log.info("Traitement d'une nouvelle demande de contact");
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateContactForm(form);
|
||||||
|
Contact contact = createContact(form);
|
||||||
|
|
||||||
|
sendConfirmationEmails(contact);
|
||||||
|
notifyContactSubmission(contact);
|
||||||
|
|
||||||
|
log.info("Demande de contact traitée avec succès - ID: {}",
|
||||||
|
contact.getId());
|
||||||
|
return contact;
|
||||||
|
|
||||||
|
} catch (BusinessException be) {
|
||||||
|
log.warn("Erreur de validation du formulaire de contact", be);
|
||||||
|
throw be;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors du traitement de la demande de contact", e);
|
||||||
|
throw new BusinessException(
|
||||||
|
"Impossible de traiter la demande de contact", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide les données du formulaire de contact.
|
||||||
|
*/
|
||||||
|
private void validateContactForm(ContactForm form) {
|
||||||
|
if (form.getName() == null || form.getName().trim().length() < 2) {
|
||||||
|
throw new BusinessException("Le nom doit contenir au moins 2 caractères");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidEmail(form.getEmail())) {
|
||||||
|
throw new BusinessException("L'adresse email n'est pas valide");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.getMessage() == null ||
|
||||||
|
form.getMessage().trim().length() < 10 ||
|
||||||
|
form.getMessage().length() > 1000) {
|
||||||
|
throw new BusinessException(
|
||||||
|
"Le message doit contenir entre 10 et 1000 caractères");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle entité Contact à partir du formulaire.
|
||||||
|
*/
|
||||||
|
private Contact createContact(ContactForm form) {
|
||||||
|
Contact contact = new Contact(
|
||||||
|
form.getName(),
|
||||||
|
form.getEmail(),
|
||||||
|
form.getSubject(),
|
||||||
|
form.getMessage()
|
||||||
|
);
|
||||||
|
|
||||||
|
contact.setStatus(ContactStatus.NEW);
|
||||||
|
contact.setSubmitDate(LocalDateTime.now());
|
||||||
|
|
||||||
|
return contactRepository.save(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie les emails de confirmation.
|
||||||
|
*/
|
||||||
|
private void sendConfirmationEmails(Contact contact) {
|
||||||
|
sendCustomerConfirmation(contact);
|
||||||
|
sendAdminNotification(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie l'email de confirmation au client.
|
||||||
|
*/
|
||||||
|
private void sendCustomerConfirmation(Contact contact) {
|
||||||
|
EmailTemplate template = EmailTemplate.builder()
|
||||||
|
.templateName("contact-confirmation")
|
||||||
|
.recipient(contact.getEmail())
|
||||||
|
.subject("Confirmation de votre message")
|
||||||
|
.parameters(Map.of(
|
||||||
|
"name", contact.getName(),
|
||||||
|
"subject", contact.getSubject(),
|
||||||
|
"message", contact.getMessage(),
|
||||||
|
"contactId", contact.getId().toString()
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
emailService.sendTemplatedEmail(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie l'email de notification à l'administrateur.
|
||||||
|
*/
|
||||||
|
private void sendAdminNotification(Contact contact) {
|
||||||
|
EmailTemplate template = EmailTemplate.builder()
|
||||||
|
.templateName("admin-contact-notification")
|
||||||
|
.recipient(emailService.config.getAdminEmailAddress())
|
||||||
|
.subject("Nouvelle demande de contact")
|
||||||
|
.parameters(Map.of(
|
||||||
|
"name", contact.getName(),
|
||||||
|
"email", contact.getEmail(),
|
||||||
|
"subject", contact.getSubject(),
|
||||||
|
"message", contact.getMessage(),
|
||||||
|
"timestamp", contact.getSubmitDate().toString(),
|
||||||
|
"contactId", contact.getId().toString()
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
emailService.sendTemplatedEmail(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifie le système de la soumission d'un nouveau contact.
|
||||||
|
*/
|
||||||
|
private void notifyContactSubmission(Contact contact) {
|
||||||
|
contactEvent.fire(new ContactSubmissionEvent(contact));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si une adresse email est valide.
|
||||||
|
*/
|
||||||
|
private boolean isValidEmail(String email) {
|
||||||
|
if (email == null || email.trim().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$";
|
||||||
|
return email.matches(emailRegex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le statut d'un contact.
|
||||||
|
*
|
||||||
|
* @param contactId Identifiant du contact
|
||||||
|
* @param newStatus Nouveau statut
|
||||||
|
* @param note Note optionnelle sur la mise à jour
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void updateContactStatus(
|
||||||
|
@NotNull Long contactId,
|
||||||
|
@NotNull ContactStatus newStatus,
|
||||||
|
String note) {
|
||||||
|
|
||||||
|
log.info("Mise à jour du statut du contact {} vers {}",
|
||||||
|
contactId, newStatus);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Contact contact = contactRepository.findById(contactId)
|
||||||
|
.orElseThrow(() -> new BusinessException("Contact non trouvé"));
|
||||||
|
|
||||||
|
contact.setStatus(newStatus);
|
||||||
|
contact.setProcessDate(LocalDateTime.now());
|
||||||
|
|
||||||
|
if (note != null && !note.trim().isEmpty()) {
|
||||||
|
addInternalNote(contact, note);
|
||||||
|
}
|
||||||
|
|
||||||
|
contactRepository.update(contact);
|
||||||
|
|
||||||
|
log.info("Statut du contact mis à jour avec succès");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la mise à jour du statut du contact", e);
|
||||||
|
throw new BusinessException(
|
||||||
|
"Impossible de mettre à jour le statut du contact", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une note interne à un contact.
|
||||||
|
*/
|
||||||
|
private void addInternalNote(Contact contact, String note) {
|
||||||
|
String currentNotes = contact.getInternalNotes();
|
||||||
|
String timestamp = LocalDateTime.now().toString();
|
||||||
|
String newNote = String.format("[%s] %s", timestamp, note);
|
||||||
|
|
||||||
|
if (currentNotes == null || currentNotes.trim().isEmpty()) {
|
||||||
|
contact.setInternalNotes(newNote);
|
||||||
|
} else {
|
||||||
|
contact.setInternalNotes(currentNotes + "\n" + newNote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
201
src/main/java/dev/lions/services/EmailService.java
Normal file
201
src/main/java/dev/lions/services/EmailService.java
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package dev.lions.services;
|
||||||
|
|
||||||
|
import dev.lions.models.EmailMessage;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.mail.Message;
|
||||||
|
import jakarta.mail.MessagingException;
|
||||||
|
import jakarta.mail.Session;
|
||||||
|
import jakarta.mail.Transport;
|
||||||
|
import jakarta.mail.internet.InternetAddress;
|
||||||
|
import jakarta.mail.internet.MimeMessage;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import dev.lions.config.ApplicationConfig;
|
||||||
|
import dev.lions.models.EmailTemplate;
|
||||||
|
import dev.lions.models.Notification;
|
||||||
|
import dev.lions.exceptions.EmailException;
|
||||||
|
import dev.lions.utils.TemplateProcessor;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service gérant l'envoi des emails dans l'application.
|
||||||
|
* Cette classe assure la configuration SMTP, le traitement des modèles
|
||||||
|
* et l'envoi sécurisé des emails.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class EmailService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ApplicationConfig config;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
TemplateProcessor templateProcessor;
|
||||||
|
|
||||||
|
private static final int MAX_RETRY_ATTEMPTS = 3;
|
||||||
|
private static final long RETRY_DELAY_MS = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un email basé sur un modèle.
|
||||||
|
*
|
||||||
|
* @param template Modèle d'email à utiliser
|
||||||
|
*/
|
||||||
|
public void sendTemplatedEmail(@Valid @NotNull EmailTemplate template) {
|
||||||
|
log.info("Préparation de l'envoi d'email avec le modèle : {}",
|
||||||
|
template.getTemplateName());
|
||||||
|
|
||||||
|
try {
|
||||||
|
String htmlContent = processTemplate(template);
|
||||||
|
|
||||||
|
EmailMessage message = EmailMessage.builder()
|
||||||
|
.from(config.getSystemEmailAddress())
|
||||||
|
.to(template.getRecipient())
|
||||||
|
.subject(template.getSubject())
|
||||||
|
.htmlContent(htmlContent)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
sendEmailWithRetry(message);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de l'envoi de l'email", e);
|
||||||
|
throw new EmailException("Impossible d'envoyer l'email");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traite le contenu du modèle avec les paramètres fournis.
|
||||||
|
*/
|
||||||
|
private String processTemplate(EmailTemplate template) {
|
||||||
|
log.debug("Traitement du modèle d'email : {}", template.getTemplateName());
|
||||||
|
return templateProcessor.process(
|
||||||
|
template.getContent(),
|
||||||
|
template.getParameters()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un email avec mécanisme de reprise en cas d'échec.
|
||||||
|
*/
|
||||||
|
private void sendEmailWithRetry(EmailMessage message) {
|
||||||
|
Exception lastException = null;
|
||||||
|
|
||||||
|
for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
|
||||||
|
try {
|
||||||
|
sendEmail(message);
|
||||||
|
log.info("Email envoyé avec succès à : {}", message.getTo());
|
||||||
|
return;
|
||||||
|
} catch (Exception e) {
|
||||||
|
lastException = e;
|
||||||
|
log.warn("Échec de l'envoi (tentative {}/{})",
|
||||||
|
attempt, MAX_RETRY_ATTEMPTS);
|
||||||
|
|
||||||
|
if (attempt < MAX_RETRY_ATTEMPTS) {
|
||||||
|
sleep(RETRY_DELAY_MS * attempt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new EmailException(
|
||||||
|
"Échec de l'envoi après " + MAX_RETRY_ATTEMPTS + " tentatives");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie effectif de l'email via SMTP.
|
||||||
|
*/
|
||||||
|
private void sendEmail(EmailMessage message) throws MessagingException {
|
||||||
|
Properties props = configureSmtpProperties();
|
||||||
|
Session session = createSmtpSession(props);
|
||||||
|
|
||||||
|
MimeMessage mimeMessage = new MimeMessage(session);
|
||||||
|
configureMimeMessage(mimeMessage, message);
|
||||||
|
|
||||||
|
Transport.send(mimeMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure les propriétés SMTP.
|
||||||
|
*/
|
||||||
|
private Properties configureSmtpProperties() {
|
||||||
|
Properties props = new Properties();
|
||||||
|
props.put("mail.smtp.host", config.getSmtpHost());
|
||||||
|
props.put("mail.smtp.port", config.getSmtpPort());
|
||||||
|
props.put("mail.smtp.auth", "true");
|
||||||
|
props.put("mail.smtp.starttls.enable", "true");
|
||||||
|
props.put("mail.smtp.connectiontimeout", "5000");
|
||||||
|
props.put("mail.smtp.timeout", "5000");
|
||||||
|
|
||||||
|
if (config.isSmtpSslEnabled()) {
|
||||||
|
props.put("mail.smtp.ssl.enable", "true");
|
||||||
|
props.put("mail.smtp.ssl.trust", config.getSmtpHost());
|
||||||
|
}
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une session SMTP authentifiée.
|
||||||
|
*/
|
||||||
|
private Session createSmtpSession(Properties props) {
|
||||||
|
return Session.getInstance(props, new jakarta.mail.Authenticator() {
|
||||||
|
@Override
|
||||||
|
protected jakarta.mail.PasswordAuthentication getPasswordAuthentication() {
|
||||||
|
return new jakarta.mail.PasswordAuthentication(
|
||||||
|
config.getSmtpUsername().orElseThrow(() ->
|
||||||
|
new EmailException("Nom d'utilisateur SMTP manquant")),
|
||||||
|
config.getSmtpPassword().orElseThrow(() ->
|
||||||
|
new EmailException("Mot de passe SMTP manquant"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure le message MIME avec les paramètres fournis.
|
||||||
|
*/
|
||||||
|
private void configureMimeMessage(MimeMessage mimeMessage, EmailMessage message)
|
||||||
|
throws MessagingException {
|
||||||
|
mimeMessage.setFrom(new InternetAddress(message.getFrom()));
|
||||||
|
mimeMessage.setRecipients(
|
||||||
|
Message.RecipientType.TO,
|
||||||
|
InternetAddress.parse(message.getTo())
|
||||||
|
);
|
||||||
|
mimeMessage.setSubject(message.getSubject());
|
||||||
|
mimeMessage.setContent(message.getHtmlContent(), "text/html; charset=utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie une notification par email.
|
||||||
|
*/
|
||||||
|
public void sendNotificationEmail(@NotNull Notification notification) {
|
||||||
|
EmailTemplate template = EmailTemplate.builder()
|
||||||
|
.templateName("notification-email")
|
||||||
|
.recipient(config.getAdminEmailAddress())
|
||||||
|
.subject("Notification système : " + notification.getTitle())
|
||||||
|
.parameters(Map.of(
|
||||||
|
"title", notification.getTitle(),
|
||||||
|
"message", notification.getMessage(),
|
||||||
|
"type", notification.getType().toString(),
|
||||||
|
"timestamp", notification.getTimestamp().toString(),
|
||||||
|
"actionUrl", notification.getActionUrl()
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
sendTemplatedEmail(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sleep(long milliseconds) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(milliseconds);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new EmailException("Interruption pendant la reprise");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
33
src/main/java/dev/lions/services/FileStorageService.java
Normal file
33
src/main/java/dev/lions/services/FileStorageService.java
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package dev.lions.services;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service pour la gestion du stockage des fichiers.
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public interface FileStorageService {
|
||||||
|
void storeFile(String fileName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stocke un fichier dans le répertoire spécifié.
|
||||||
|
*/
|
||||||
|
Path storeFile(InputStream fileStream, String directory, String fileName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un répertoire temporaire pour le stockage des fichiers.
|
||||||
|
*/
|
||||||
|
String createTempDirectory(String prefix);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un fichier donné.
|
||||||
|
*/
|
||||||
|
void deleteFile(Path filePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un répertoire et son contenu.
|
||||||
|
*/
|
||||||
|
void deleteDirectory(String directoryPath);
|
||||||
|
}
|
||||||
66
src/main/java/dev/lions/services/FileStorageServiceImpl.java
Normal file
66
src/main/java/dev/lions/services/FileStorageServiceImpl.java
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package dev.lions.services;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implémentation du service de stockage de fichiers.
|
||||||
|
* Cette classe fournit des méthodes pour stocker des fichiers de manière basique.
|
||||||
|
*
|
||||||
|
* <p>Elle est annotée avec {@link ApplicationScoped}, ce qui signifie que
|
||||||
|
* Quarkus gère son cycle de vie et garantit qu'une seule instance est créée
|
||||||
|
* pour toute l'application.</p>
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class FileStorageServiceImpl implements FileStorageService {
|
||||||
|
|
||||||
|
// Logger pour suivre les actions effectuées
|
||||||
|
private static final Logger LOG = Logger.getLogger(FileStorageServiceImpl.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode pour stocker un fichier.
|
||||||
|
*
|
||||||
|
* @param fileName Nom du fichier à stocker.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void storeFile(String fileName) {
|
||||||
|
// Log d'entrée pour la méthode
|
||||||
|
LOG.info("Début du stockage du fichier : " + fileName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulation d'un stockage de fichier
|
||||||
|
System.out.println("Fichier stocké avec succès : " + fileName);
|
||||||
|
|
||||||
|
// Log de succès
|
||||||
|
LOG.info("Le fichier a été stocké avec succès.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Gestion des erreurs avec log
|
||||||
|
LOG.error("Erreur lors du stockage du fichier : " + fileName, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log de sortie pour la méthode
|
||||||
|
LOG.debug("Fin du traitement de la méthode storeFile.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Path storeFile(InputStream fileStream, String directory, String fileName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String createTempDirectory(String prefix) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteFile(Path filePath) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteDirectory(String directoryPath) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
189
src/main/java/dev/lions/services/NotificationService.java
Normal file
189
src/main/java/dev/lions/services/NotificationService.java
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
package dev.lions.services;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import dev.lions.models.Notification;
|
||||||
|
import dev.lions.models.NotificationStatus;
|
||||||
|
import dev.lions.models.NotificationType;
|
||||||
|
import dev.lions.repositories.NotificationRepository;
|
||||||
|
import dev.lions.dtos.NotificationDTO;
|
||||||
|
import dev.lions.exceptions.NotificationException;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service gérant les notifications système de l'application.
|
||||||
|
* Cette classe assure la création, l'envoi et le suivi des notifications
|
||||||
|
* avec support pour différents types de notifications et canaux de distribution.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
public class NotificationService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
NotificationRepository notificationRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
WebSocketService webSocketService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
EmailService emailService;
|
||||||
|
|
||||||
|
private static final int MAX_BATCH_SIZE = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée et envoie une nouvelle notification interne.
|
||||||
|
*
|
||||||
|
* @param type Type de notification
|
||||||
|
* @param message Contenu de la notification
|
||||||
|
* @return Notification créée
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Notification sendInternalNotification(
|
||||||
|
@NotNull NotificationType type,
|
||||||
|
@NotNull String message) {
|
||||||
|
|
||||||
|
log.info("Création d'une notification interne de type : {}", type);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Notification notification = createNotification(type, message);
|
||||||
|
notification = notificationRepository.save(notification);
|
||||||
|
|
||||||
|
distributeNotification(notification);
|
||||||
|
|
||||||
|
log.info("Notification créée et distribuée avec succès - ID: {}",
|
||||||
|
notification.getId());
|
||||||
|
return notification;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de l'envoi de la notification interne", e);
|
||||||
|
throw new NotificationException(
|
||||||
|
"Impossible d'envoyer la notification interne", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle notification.
|
||||||
|
*/
|
||||||
|
private Notification createNotification(NotificationType type, String message) {
|
||||||
|
return Notification.builder()
|
||||||
|
.title(generateTitle(type))
|
||||||
|
.message(message)
|
||||||
|
.type(type)
|
||||||
|
.status(NotificationStatus.UNREAD)
|
||||||
|
.timestamp(LocalDateTime.now())
|
||||||
|
.data(createNotificationData())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distribue la notification via différents canaux.
|
||||||
|
*/
|
||||||
|
private void distributeNotification(Notification notification) {
|
||||||
|
// Envoi WebSocket pour mise à jour en temps réel
|
||||||
|
webSocketService.broadcastToAdmins(NotificationDTO.from(notification));
|
||||||
|
|
||||||
|
// Envoi d'email pour les notifications critiques
|
||||||
|
if (notification.isCritical()) {
|
||||||
|
emailService.sendNotificationEmail(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un titre approprié selon le type de notification.
|
||||||
|
*/
|
||||||
|
private String generateTitle(NotificationType type) {
|
||||||
|
return switch (type) {
|
||||||
|
case NEW_CONTACT -> "Nouveau message de contact";
|
||||||
|
case SYSTEM_ALERT -> "Alerte système";
|
||||||
|
case SECURITY_ALERT -> "Alerte de sécurité";
|
||||||
|
default -> "Notification";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée les données additionnelles de la notification.
|
||||||
|
*/
|
||||||
|
private Notification.NotificationData createNotificationData() {
|
||||||
|
return Notification.NotificationData.builder()
|
||||||
|
.attributes(new HashMap<>())
|
||||||
|
.metadata(new HashMap<>())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les notifications non lues.
|
||||||
|
*/
|
||||||
|
public List<Notification> getUnreadNotifications() {
|
||||||
|
log.debug("Récupération des notifications non lues");
|
||||||
|
return notificationRepository.findUnreadNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marque une notification comme lue.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void markAsRead(@NotNull Long notificationId) {
|
||||||
|
log.debug("Marquage de la notification {} comme lue", notificationId);
|
||||||
|
notificationRepository.markAsRead(notificationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les notifications critiques actives.
|
||||||
|
*/
|
||||||
|
public List<Notification> getCriticalNotifications() {
|
||||||
|
log.debug("Récupération des notifications critiques");
|
||||||
|
return notificationRepository.findCriticalNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie les anciennes notifications.
|
||||||
|
*
|
||||||
|
* @param retentionDays Nombre de jours de rétention
|
||||||
|
* @return Nombre de notifications supprimées
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public int cleanupOldNotifications(int retentionDays) {
|
||||||
|
log.info("Nettoyage des notifications plus anciennes que {} jours",
|
||||||
|
retentionDays);
|
||||||
|
|
||||||
|
LocalDateTime retentionDate = LocalDateTime.now()
|
||||||
|
.minusDays(retentionDays);
|
||||||
|
|
||||||
|
try {
|
||||||
|
int deletedCount = notificationRepository
|
||||||
|
.deleteEventsOlderThan(retentionDate);
|
||||||
|
|
||||||
|
log.info("{} notifications anciennes supprimées", deletedCount);
|
||||||
|
return deletedCount;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors du nettoyage des notifications", e);
|
||||||
|
throw new NotificationException(
|
||||||
|
"Impossible de nettoyer les anciennes notifications", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les statistiques des notifications.
|
||||||
|
*/
|
||||||
|
public Map<NotificationType, Long> getNotificationStatistics(
|
||||||
|
LocalDateTime startDate,
|
||||||
|
LocalDateTime endDate) {
|
||||||
|
|
||||||
|
log.debug("Calcul des statistiques de notifications entre {} et {}",
|
||||||
|
startDate, endDate);
|
||||||
|
|
||||||
|
return notificationRepository.countByType(startDate, endDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
241
src/main/java/dev/lions/services/ProjectService.java
Normal file
241
src/main/java/dev/lions/services/ProjectService.java
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
package dev.lions.services;
|
||||||
|
|
||||||
|
import dev.lions.events.ProjectUpdateEvent;
|
||||||
|
import dev.lions.exceptions.BusinessException;
|
||||||
|
import dev.lions.models.Project;
|
||||||
|
import dev.lions.models.Testimonial;
|
||||||
|
import dev.lions.repositories.ProjectRepository;
|
||||||
|
import dev.lions.utils.ImageProcessor;
|
||||||
|
import dev.lions.config.ApplicationConfig;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.event.Event;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service gérant la logique métier des projets.
|
||||||
|
* Cette classe assure la gestion complète du cycle de vie des projets,
|
||||||
|
* incluant leur création, mise à jour, recherche et validation.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
@Builder
|
||||||
|
public class ProjectService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ProjectRepository projectRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ImageProcessor imageProcessor;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ApplicationConfig config;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Event<ProjectUpdateEvent> projectUpdateEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un nouveau projet avec son image associée.
|
||||||
|
*
|
||||||
|
* @param project Données du projet
|
||||||
|
* @param imageData Image du projet en bytes
|
||||||
|
* @return Projet créé
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Project createProject(@Valid @NotNull Project project, byte[] imageData) {
|
||||||
|
log.info("Création d'un nouveau projet : {}", project.getTitle());
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateProject(project);
|
||||||
|
processProjectImage(project, imageData);
|
||||||
|
|
||||||
|
Project savedProject = projectRepository.save(project);
|
||||||
|
notifyProjectCreation(savedProject);
|
||||||
|
|
||||||
|
log.info("Projet créé avec succès - ID: {}", savedProject.getId());
|
||||||
|
return savedProject;
|
||||||
|
|
||||||
|
} catch (BusinessException be) {
|
||||||
|
log.warn("Validation échouée pour le projet", be);
|
||||||
|
throw be;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la création du projet", e);
|
||||||
|
throw new BusinessException("Impossible de créer le projet", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour un projet existant.
|
||||||
|
*
|
||||||
|
* @param project Projet à mettre à jour
|
||||||
|
* @param imageData Nouvelle image optionnelle
|
||||||
|
* @return Projet mis à jour
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Project updateProject(@Valid @NotNull Project project, byte[] imageData) {
|
||||||
|
log.info("Mise à jour du projet : {}", project.getId());
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateProject(project);
|
||||||
|
|
||||||
|
if (imageData != null) {
|
||||||
|
processProjectImage(project, imageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
Project updatedProject = projectRepository.update(project);
|
||||||
|
notifyProjectUpdate(updatedProject);
|
||||||
|
|
||||||
|
log.info("Projet mis à jour avec succès - ID: {}", updatedProject.getId());
|
||||||
|
return updatedProject;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la mise à jour du projet", e);
|
||||||
|
throw new BusinessException("Impossible de mettre à jour le projet", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traite et stocke l'image du projet.
|
||||||
|
*/
|
||||||
|
private void processProjectImage(Project project, byte[] imageData) {
|
||||||
|
if (imageData == null || imageData.length == 0) {
|
||||||
|
throw new BusinessException("L'image du projet est requise");
|
||||||
|
}
|
||||||
|
|
||||||
|
String imageUrl = imageProcessor.processAndStoreProjectImage(
|
||||||
|
imageData,
|
||||||
|
project.getTitle(),
|
||||||
|
config.getImageStoragePath()
|
||||||
|
);
|
||||||
|
|
||||||
|
project.setImageUrl(imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un projet par son identifiant.
|
||||||
|
*/
|
||||||
|
public Optional<Project> findById(@NotNull String projectId) {
|
||||||
|
log.debug("Recherche du projet : {}", projectId);
|
||||||
|
return projectRepository.findById(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie l'existence d'un projet.
|
||||||
|
*/
|
||||||
|
public boolean existsById(@NotNull String projectId) {
|
||||||
|
return projectRepository.existsById(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les projets filtrés par tag.
|
||||||
|
*/
|
||||||
|
public List<Project> getFilteredProjects(String filter) {
|
||||||
|
log.debug("Filtrage des projets avec le critère : {}", filter);
|
||||||
|
|
||||||
|
if ("all".equalsIgnoreCase(filter)) {
|
||||||
|
return projectRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectRepository.findByTags(Set.of(filter.toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les projets mis en avant.
|
||||||
|
*/
|
||||||
|
public List<Project> getFeaturedProjects() {
|
||||||
|
log.debug("Récupération des projets mis en avant");
|
||||||
|
return projectRepository.findByFeatured(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les projets récents.
|
||||||
|
*/
|
||||||
|
public List<Project> getRecentProjects(int limit) {
|
||||||
|
log.debug("Récupération des {} projets les plus récents", limit);
|
||||||
|
return projectRepository.findRecentProjects(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le nombre total de projets.
|
||||||
|
*/
|
||||||
|
public long getProjectCount() {
|
||||||
|
log.debug("Récupération du nombre total de projets");
|
||||||
|
return projectRepository.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les témoignages mis en avant.
|
||||||
|
*/
|
||||||
|
public List<Testimonial> getFeaturedTestimonials() {
|
||||||
|
log.debug("Récupération des témoignages mis en avant");
|
||||||
|
|
||||||
|
return projectRepository.findByFeatured(true).stream()
|
||||||
|
.filter(project -> !project.getTestimonials().isEmpty())
|
||||||
|
.map(this::createTestimonialFromProject)
|
||||||
|
.limit(3)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide les données d'un projet.
|
||||||
|
*/
|
||||||
|
private void validateProject(Project project) {
|
||||||
|
if (project.getTitle() == null || project.getTitle().trim().isEmpty()) {
|
||||||
|
throw new BusinessException("Le titre du projet est requis");
|
||||||
|
}
|
||||||
|
if (project.getDescription() == null || project.getDescription().trim().isEmpty()) {
|
||||||
|
throw new BusinessException("La description du projet est requise");
|
||||||
|
}
|
||||||
|
if (project.getShortDescription() == null ||
|
||||||
|
project.getShortDescription().trim().isEmpty()) {
|
||||||
|
throw new BusinessException("La description courte du projet est requise");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un témoignage à partir d'un projet.
|
||||||
|
*/
|
||||||
|
private Testimonial createTestimonialFromProject(Project project) {
|
||||||
|
return Testimonial.builder()
|
||||||
|
.clientName(project.getClientName())
|
||||||
|
.content(project.getTestimonials().get(0))
|
||||||
|
.projectTitle(project.getTitle())
|
||||||
|
.date(project.getCompletionDate())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifie la création d'un projet.
|
||||||
|
*/
|
||||||
|
private void notifyProjectCreation(Project project) {
|
||||||
|
projectUpdateEvent.fire(new ProjectUpdateEvent(
|
||||||
|
project.getId(),
|
||||||
|
"CREATE",
|
||||||
|
LocalDateTime.now()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifie la mise à jour d'un projet.
|
||||||
|
*/
|
||||||
|
private void notifyProjectUpdate(Project project) {
|
||||||
|
projectUpdateEvent.fire(new ProjectUpdateEvent(
|
||||||
|
project.getId(),
|
||||||
|
"UPDATE",
|
||||||
|
LocalDateTime.now()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
209
src/main/java/dev/lions/services/WebSocketService.java
Normal file
209
src/main/java/dev/lions/services/WebSocketService.java
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
package dev.lions.services;
|
||||||
|
|
||||||
|
import dev.lions.dtos.NotificationDTO;
|
||||||
|
import dev.lions.exceptions.WebSocketException;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.websocket.*;
|
||||||
|
import jakarta.websocket.server.ServerEndpoint;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service gérant les communications WebSocket de l'application.
|
||||||
|
* Cette classe assure la gestion des connexions temps réel et la diffusion
|
||||||
|
* des notifications aux clients connectés.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ApplicationScoped
|
||||||
|
@ServerEndpoint("/ws/notifications")
|
||||||
|
public class WebSocketService {
|
||||||
|
|
||||||
|
private static final Map<String, Session> sessions = new ConcurrentHashMap<>();
|
||||||
|
private static final int MAX_MESSAGE_SIZE = 8192;
|
||||||
|
private static final long IDLE_TIMEOUT = 300000; // 5 minutes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère l'ouverture d'une nouvelle connexion WebSocket.
|
||||||
|
*
|
||||||
|
* @param session Session WebSocket ouverte
|
||||||
|
*/
|
||||||
|
@OnOpen
|
||||||
|
public void onOpen(Session session) {
|
||||||
|
log.info("Nouvelle connexion WebSocket établie : {}", session.getId());
|
||||||
|
|
||||||
|
try {
|
||||||
|
configureSession(session);
|
||||||
|
sessions.put(session.getId(), session);
|
||||||
|
|
||||||
|
sendWelcomeMessage(session);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de l'initialisation de la session WebSocket", e);
|
||||||
|
closeSession(session, "Erreur d'initialisation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure une nouvelle session WebSocket.
|
||||||
|
*/
|
||||||
|
private void configureSession(Session session) {
|
||||||
|
session.setMaxIdleTimeout(IDLE_TIMEOUT);
|
||||||
|
session.setMaxTextMessageBufferSize(MAX_MESSAGE_SIZE);
|
||||||
|
session.setMaxBinaryMessageBufferSize(MAX_MESSAGE_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un message de bienvenue au client connecté.
|
||||||
|
*/
|
||||||
|
private void sendWelcomeMessage(Session session) {
|
||||||
|
try {
|
||||||
|
String message = "{\"type\":\"welcome\",\"message\":\"Connexion établie\"}";
|
||||||
|
session.getBasicRemote().sendText(message);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Impossible d'envoyer le message de bienvenue", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère la réception d'un message depuis un client.
|
||||||
|
*
|
||||||
|
* @param message Message reçu
|
||||||
|
* @param session Session WebSocket active
|
||||||
|
*/
|
||||||
|
@OnMessage
|
||||||
|
public void onMessage(String message, Session session) {
|
||||||
|
String sessionId = session.getId();
|
||||||
|
log.debug("Message reçu de la session {} : {}", sessionId, message);
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateMessage(message);
|
||||||
|
processMessage(message, session);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors du traitement du message", e);
|
||||||
|
sendErrorResponse(session, "Erreur de traitement du message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide le contenu d'un message reçu.
|
||||||
|
*/
|
||||||
|
private void validateMessage(String message) {
|
||||||
|
if (message == null || message.trim().isEmpty()) {
|
||||||
|
throw new WebSocketException("Message vide non autorisé");
|
||||||
|
}
|
||||||
|
if (message.length() > MAX_MESSAGE_SIZE) {
|
||||||
|
throw new WebSocketException("Message trop long");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traite un message reçu.
|
||||||
|
*/
|
||||||
|
private void processMessage(String message, Session session) {
|
||||||
|
// Implémentation du traitement des messages selon les besoins
|
||||||
|
log.debug("Traitement du message de la session {}", session.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère la fermeture d'une connexion WebSocket.
|
||||||
|
*
|
||||||
|
* @param session Session WebSocket fermée
|
||||||
|
*/
|
||||||
|
@OnClose
|
||||||
|
public void onClose(Session session) {
|
||||||
|
String sessionId = session.getId();
|
||||||
|
log.info("Fermeture de la connexion WebSocket : {}", sessionId);
|
||||||
|
|
||||||
|
sessions.remove(sessionId);
|
||||||
|
cleanupSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les erreurs survenant sur une connexion WebSocket.
|
||||||
|
*
|
||||||
|
* @param session Session WebSocket concernée
|
||||||
|
* @param throwable Erreur survenue
|
||||||
|
*/
|
||||||
|
@OnError
|
||||||
|
public void onError(Session session, Throwable throwable) {
|
||||||
|
String sessionId = session.getId();
|
||||||
|
log.error("Erreur WebSocket sur la session {} : {}",
|
||||||
|
sessionId, throwable.getMessage(), throwable);
|
||||||
|
|
||||||
|
closeSession(session, "Erreur interne");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diffuse une notification à tous les clients connectés.
|
||||||
|
*
|
||||||
|
* @param notification Notification à diffuser
|
||||||
|
*/
|
||||||
|
public void broadcastToAdmins(NotificationDTO notification) {
|
||||||
|
log.debug("Diffusion d'une notification à {} clients", sessions.size());
|
||||||
|
|
||||||
|
String message = notification.toJson();
|
||||||
|
sessions.values().forEach(session -> {
|
||||||
|
try {
|
||||||
|
if (session.isOpen()) {
|
||||||
|
session.getBasicRemote().sendText(message);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Erreur lors de l'envoi à la session {}",
|
||||||
|
session.getId(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie une réponse d'erreur à un client.
|
||||||
|
*/
|
||||||
|
private void sendErrorResponse(Session session, String errorMessage) {
|
||||||
|
try {
|
||||||
|
String message = String.format(
|
||||||
|
"{\"type\":\"error\",\"message\":\"%s\"}",
|
||||||
|
errorMessage
|
||||||
|
);
|
||||||
|
session.getBasicRemote().sendText(message);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Impossible d'envoyer la réponse d'erreur", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ferme une session WebSocket.
|
||||||
|
*/
|
||||||
|
private void closeSession(Session session, String reason) {
|
||||||
|
try {
|
||||||
|
session.close(new CloseReason(
|
||||||
|
CloseReason.CloseCodes.NORMAL_CLOSURE,
|
||||||
|
reason
|
||||||
|
));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Erreur lors de la fermeture de la session", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie les ressources d'une session.
|
||||||
|
*/
|
||||||
|
private void cleanupSession(Session session) {
|
||||||
|
try {
|
||||||
|
session.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Erreur lors du nettoyage de la session", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le nombre de clients connectés.
|
||||||
|
*/
|
||||||
|
public int getConnectedClientsCount() {
|
||||||
|
return sessions.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user