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 getTags() { return Collections.unmodifiableList(tags); } /** * Récupère les technologies de manière sécurisée. * * @return Liste immuable des technologies */ public List 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 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 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; } }