ProductImage.java
package com.ctrlbuy.webshop.entity;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.ToString;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* ProductImage Entity - Railway-optimerad med Lombok och förbättrad funktionalitet
* ✅ UPPDATERAD: Lombok integration, business logic, och Railway-kompatibilitet
* ✅ FIXAD: Index-namn matchar faktiska kolumnnamn (display_order)
*/
@Entity
@Table(name = "product_images", indexes = {
@Index(name = "idx_product_image_product", columnList = "product_id"),
@Index(name = "idx_product_image_primary", columnList = "isPrimary"),
@Index(name = "idx_product_image_order", columnList = "display_order"), // ✅ FIXAD: display_order
@Index(name = "idx_product_image_type", columnList = "imageType")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString(exclude = {"product"}) // Undvik lazy loading i toString
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class ProductImage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@EqualsAndHashCode.Include
private Long id;
// ============================
// PRODUCT RELATIONSHIP
// ============================
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false,
foreignKey = @ForeignKey(name = "fk_product_image_product"))
private Product product;
// ============================
// IMAGE INFORMATION
// ============================
@Column(nullable = false, length = 500)
@EqualsAndHashCode.Include
private String imageUrl;
@Column(length = 255)
private String altText;
@Column(length = 50)
@Enumerated(EnumType.STRING)
@Builder.Default
private ImageType imageType = ImageType.PRODUCT;
@Column(length = 100)
private String fileName;
@Column(length = 50)
private String mimeType;
// ============================
// IMAGE DIMENSIONS & METADATA
// ============================
@Column(name = "width")
private Integer width;
@Column(name = "height")
private Integer height;
@Column(name = "file_size")
private Long fileSize;
@Column(length = 20)
private String resolution;
@Column(precision = 3)
private Float aspectRatio;
// ============================
// DISPLAY CONFIGURATION
// ============================
@Column(name = "isPrimary")
@Builder.Default
private Boolean isPrimary = false;
@Column(name = "display_order")
@Builder.Default
private Integer displayOrder = 0;
@Column(name = "sort_order")
@Builder.Default
private Integer sortOrder = 0;
@Column(name = "isActive")
@Builder.Default
private Boolean isActive = true;
@Column(name = "isLocked")
@Builder.Default
private Boolean isLocked = false;
@Column(name = "isVisible")
@Builder.Default
private Boolean isVisible = true;
// ============================
// IMAGE VARIANTS
// ============================
@Column(length = 500)
private String thumbnailUrl;
@Column(length = 500)
private String mediumUrl;
@Column(length = 500)
private String largeUrl;
@Column(length = 500)
private String originalUrl;
// ============================
// SEO & ACCESSIBILITY
// ============================
@Column(length = 100)
private String title;
@Column(length = 500)
private String description;
@Column(length = 200)
private String caption;
@Column(length = 100)
private String photographer;
@Column(length = 100)
private String source;
// ============================
// AUDIT FIELDS
// ============================
@Column(updatable = false, name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "uploaded_at")
private LocalDateTime uploadedAt;
// ============================
// LIFECYCLE CALLBACKS
// ============================
@PrePersist
protected void onCreate() {
LocalDateTime now = LocalDateTime.now();
if (createdAt == null) {
createdAt = now;
}
if (updatedAt == null) {
updatedAt = now;
}
if (uploadedAt == null) {
uploadedAt = now;
}
// Set defaults
if (isPrimary == null) isPrimary = false;
if (isActive == null) isActive = true;
if (isLocked == null) isLocked = false;
if (isVisible == null) isVisible = true;
if (displayOrder == null) displayOrder = 0;
if (sortOrder == null) sortOrder = 0;
if (imageType == null) imageType = ImageType.PRODUCT;
// Calculate aspect ratio if dimensions are available
calculateAspectRatio();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
calculateAspectRatio();
}
// ============================
// ENUMS
// ============================
public enum ImageType {
PRODUCT("Product Image", "Produktbild"),
THUMBNAIL("Thumbnail", "Miniatyrbild"),
GALLERY("Gallery", "Galleri"),
DETAIL("Detail", "Detaljbild"),
LIFESTYLE("Lifestyle", "Livsstilsbild"),
TECHNICAL("Technical", "Teknisk bild"),
PACKAGING("Packaging", "Förpackning"),
INSTRUCTION("Instruction", "Instruktion"),
WARRANTY("Warranty", "Garanti"),
CERTIFICATE("Certificate", "Certifikat");
private final String displayName;
private final String swedishName;
ImageType(String displayName, String swedishName) {
this.displayName = displayName;
this.swedishName = swedishName;
}
public String getDisplayName() { return displayName; }
public String getSwedishName() { return swedishName; }
}
// ============================
// BUSINESS LOGIC METHODS
// ============================
/**
* Beräkna aspect ratio baserat på width/height
*/
private void calculateAspectRatio() {
if (width != null && height != null && height > 0) {
this.aspectRatio = width.floatValue() / height.floatValue();
}
}
/**
* Kontrollera om bilden är synlig för kunder
*/
public boolean isDisplayable() {
return Boolean.TRUE.equals(isActive) &&
Boolean.TRUE.equals(isVisible) &&
imageUrl != null && !imageUrl.trim().isEmpty();
}
/**
* Kontrollera om bilden kan redigeras
*/
public boolean isEditable() {
return !Boolean.TRUE.equals(isLocked);
}
/**
* Hämta bästa tillgängliga bild-URL baserat på storlek
*/
public String getBestImageUrl(ImageSize preferredSize) {
switch (preferredSize) {
case THUMBNAIL:
return thumbnailUrl != null ? thumbnailUrl : imageUrl;
case MEDIUM:
return mediumUrl != null ? mediumUrl :
(largeUrl != null ? largeUrl : imageUrl);
case LARGE:
return largeUrl != null ? largeUrl :
(originalUrl != null ? originalUrl : imageUrl);
case ORIGINAL:
return originalUrl != null ? originalUrl : imageUrl;
default:
return imageUrl;
}
}
/**
* Enum för bildstorlekar
*/
public enum ImageSize {
THUMBNAIL, MEDIUM, LARGE, ORIGINAL
}
/**
* Hämta formatted filstorlek
*/
public String getFormattedFileSize() {
if (fileSize == null) {
return "Unknown";
}
if (fileSize < 1024) {
return fileSize + " B";
} else if (fileSize < 1024 * 1024) {
return String.format("%.1f KB", fileSize / 1024.0);
} else {
return String.format("%.1f MB", fileSize / (1024.0 * 1024.0));
}
}
/**
* Hämta bildformat från filename eller mimeType
*/
public String getImageFormat() {
if (mimeType != null) {
switch (mimeType.toLowerCase()) {
case "image/jpeg":
case "image/jpg":
return "JPEG";
case "image/png":
return "PNG";
case "image/gif":
return "GIF";
case "image/webp":
return "WebP";
case "image/svg+xml":
return "SVG";
default:
return mimeType.substring(6).toUpperCase();
}
}
if (fileName != null) {
String extension = getFileExtension(fileName);
return extension.toUpperCase();
}
if (imageUrl != null) {
String extension = getFileExtension(imageUrl);
return extension.toUpperCase();
}
return "Unknown";
}
/**
* Extrahera filextension från filename
*/
private String getFileExtension(String filename) {
if (filename == null || filename.isEmpty()) {
return "";
}
int lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex > 0 && lastDotIndex < filename.length() - 1) {
return filename.substring(lastDotIndex + 1);
}
return "";
}
/**
* Kontrollera om bilden är i landskapsformat
*/
public boolean isLandscape() {
return aspectRatio != null && aspectRatio > 1.0f;
}
/**
* Kontrollera om bilden är i porträttformat
*/
public boolean isPortrait() {
return aspectRatio != null && aspectRatio < 1.0f;
}
/**
* Kontrollera om bilden är kvadratisk
*/
public boolean isSquare() {
return aspectRatio != null && Math.abs(aspectRatio - 1.0f) < 0.1f;
}
/**
* Hämta effective alt text (fallback till title eller description)
*/
public String getEffectiveAltText() {
if (altText != null && !altText.trim().isEmpty()) {
return altText;
}
if (title != null && !title.trim().isEmpty()) {
return title;
}
if (description != null && !description.trim().isEmpty()) {
String shortDesc = description.length() > 100 ?
description.substring(0, 97) + "..." : description;
return shortDesc;
}
// Fallback till product name om tillgängligt
if (product != null && product.getName() != null) {
return product.getName() + " - " + imageType.getSwedishName();
}
return "Produktbild";
}
/**
* Generera responsive image srcset för olika skärmar
*/
public String generateSrcSet() {
StringBuilder srcSet = new StringBuilder();
if (thumbnailUrl != null) {
srcSet.append(thumbnailUrl).append(" 300w");
}
if (mediumUrl != null) {
if (srcSet.length() > 0) srcSet.append(", ");
srcSet.append(mediumUrl).append(" 600w");
}
if (largeUrl != null) {
if (srcSet.length() > 0) srcSet.append(", ");
srcSet.append(largeUrl).append(" 1200w");
}
if (originalUrl != null) {
if (srcSet.length() > 0) srcSet.append(", ");
srcSet.append(originalUrl).append(" 1920w");
}
return srcSet.toString();
}
// ============================
// BOOLEAN ALIAS METHODS (för compatibility)
// ============================
/**
* Standard boolean naming convention aliases
*/
public Boolean isPrimary() {
return isPrimary;
}
public void setPrimary(Boolean primary) {
this.isPrimary = primary;
}
public Boolean isActive() {
return isActive;
}
public void setActive(Boolean active) {
this.isActive = active;
}
public Boolean isLocked() {
return isLocked;
}
public void setLocked(Boolean locked) {
this.isLocked = locked;
}
public Boolean isVisible() {
return isVisible;
}
public void setVisible(Boolean visible) {
this.isVisible = visible;
}
// ============================
// VALIDATION METHODS
// ============================
/**
* Validera att bilden har all nödvändig information
*/
public boolean isValid() {
return imageUrl != null && !imageUrl.trim().isEmpty() &&
product != null;
}
/**
* Kontrollera om bild-URL är giltig
*/
public boolean hasValidUrl() {
return imageUrl != null &&
!imageUrl.trim().isEmpty() &&
(imageUrl.startsWith("http://") ||
imageUrl.startsWith("https://") ||
imageUrl.startsWith("/"));
}
// ============================
// CONSTRUCTORS (additional convenience)
// ============================
/**
* Convenience constructor för basic image creation
*/
public ProductImage(Product product, String imageUrl, String altText) {
this();
this.product = product;
this.imageUrl = imageUrl;
this.altText = altText;
}
/**
* Constructor för primary image
*/
public ProductImage(Product product, String imageUrl, String altText, boolean isPrimary) {
this(product, imageUrl, altText);
this.isPrimary = isPrimary;
}
/**
* Constructor med image type
*/
public ProductImage(Product product, String imageUrl, String altText, ImageType imageType) {
this(product, imageUrl, altText);
this.imageType = imageType;
}
}