Versions stable (inachevée mais prête à un déploiement en prod)

This commit is contained in:
DahoudG
2024-12-15 16:35:50 +00:00
parent a276ac2318
commit d2cb9da730
126 changed files with 13559 additions and 631 deletions

BIN
.gitignore vendored

Binary file not shown.

415
pom.xml
View File

@@ -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>

View File

@@ -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" ]

View File

@@ -0,0 +1 @@
lionsdev_db-20241215.sql.gz

View File

@@ -0,0 +1 @@
lionsdev_db-20241215-121555.sql.gz

View File

@@ -0,0 +1 @@
lionsdev_db-202412.sql.gz

View File

@@ -0,0 +1 @@
lionsdev_db-202450.sql.gz

View 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
View 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;

View 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;
}
}
}

View 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'

View File

@@ -0,0 +1 @@
kJ9#mP2@nQ8&xR3

View File

@@ -0,0 +1 @@
lions_admin_db

View File

@@ -0,0 +1 @@
tR6#fL9@wQ7&nX2%pY5

View File

@@ -0,0 +1 @@
lions_admin

View File

@@ -0,0 +1 @@
admin@lions.dev

View File

@@ -0,0 +1 @@
hN7%pW4#cM9@jX5&vY8

View 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;
}
}

View File

@@ -0,0 +1,4 @@
package dev.lions.components;
public class DataTableView {
}

View 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);
}
}

View 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;
}
}

View 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));
}
}

View File

@@ -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);
}
}

View 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));
}
}

View 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;
}
}
}

View 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());
}
}

View File

@@ -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);
}
}

View 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);
}
}
}

View File

@@ -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;
} }
} }

View File

@@ -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());
} }
} }

View 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"
)
);
}
}

View 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);
}
}
}

View 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();
}
}

View 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;
}

View 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;
}
}

View 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()));
}
}

View 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();
}
}

View 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;
}

View 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
}

View 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);
}
}
}

View 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);
}
}

View File

@@ -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);
}
}
}

View 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;
}

View 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);
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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);
}
}

View 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();
}
}

View File

@@ -0,0 +1,7 @@
package dev.lions.exceptions;
public class EmailException extends RuntimeException {
public EmailException(String message) {
super(message);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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);
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,7 @@
package dev.lions.exceptions;
public class NavigationException extends RuntimeException {
public NavigationException(String message) {
super(message);
}
}

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,7 @@
package dev.lions.exceptions;
public class WebSocketException extends RuntimeException {
public WebSocketException(String message) {
super(message);
}
}

View 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");
}
}

View 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();
}
}

View 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"));
}
}

View 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;
}
}

View 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;
}

View 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();
}
}

View 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;
}

View 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();
}
}

View 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;
}
}

View 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;
}
}

View 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;
}

View 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;
}
}

View 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);
}
}

View 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("-+", "-");
}
}
}

View 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();
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View File

@@ -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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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é");
}
}

View 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é");
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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");
}
}
}

View 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);
}

View 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) {
}
}

View 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);
}
}

View 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()
));
}
}

View 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