Material.java

package com.university.bookstore.model;

import java.time.Year;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;

/**
 * Abstract base class representing any material in the store inventory.
 * Demonstrates abstraction and inheritance in OOP design.
 * 
 * <p>This class provides common properties and behavior for all materials
 * including books, magazines, audio, and video content.</p>
 * 
 * @author Navid Mohaghegh
 * @version 2.0
 * @since 2024-09-15
 */
@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "@class",
    visible = true
)
@JsonSubTypes({
    @Type(value = PrintedBook.class, name = "PrintedBook"),
    @Type(value = EBook.class, name = "EBook"),
    @Type(value = AudioBook.class, name = "AudioBook"),
    @Type(value = VideoMaterial.class, name = "VideoMaterial"),
    @Type(value = Magazine.class, name = "Magazine")
})
public abstract class Material implements Comparable<Material> {
    
    protected static final int MIN_YEAR = 1450;
    
    protected final String id;
    protected final String title;
    protected final double price;
    protected final int year;
    protected final MaterialType type;
    
    /**
     * Enumeration of material types for polymorphic behavior.
     */
    public enum MaterialType {
        BOOK("Book"),
        MAGAZINE("Magazine"),
        AUDIO_BOOK("Audio Book"),
        VIDEO("Video"),
        MUSIC_ALBUM("Music Album"),
        PODCAST("Podcast"),
        DOCUMENTARY("Documentary"),
        E_BOOK("E-Book"),
        EBOOK("EBook");
        
        private final String displayName;
        
        MaterialType(String displayName) {
            this.displayName = displayName;
        }
        
        public String getDisplayName() {
            return displayName;
        }
    }
    
    /**
     * Protected constructor for subclasses.
     * 
     * @param id unique identifier for the material
     * @param title the material title
     * @param price the price in dollars
     * @param year the publication/release year
     * @param type the type of material
     */
    protected Material(String id, String title, double price, int year, MaterialType type) {
        this.id = validateId(id);
        this.title = validateStringField(title, "Title");
        this.price = validatePrice(price);
        this.year = validateYear(year);
        this.type = Objects.requireNonNull(type, "Material type cannot be null");
    }
    
    /**
     * Abstract method to get the creator/author/artist of the material.
     * Implementation varies by material type.
     * 
     * @return the creator's name
     */
    public abstract String getCreator();
    
    /**
     * Abstract method to get a formatted display string.
     * Each material type should provide its own formatting.
     * 
     * @return formatted display string
     */
    public abstract String getDisplayInfo();
    
    /**
     * Template method for calculating discounted price.
     * Subclasses can override getDiscountRate() to customize.
     * 
     * @return discounted price
     */
    public final double getDiscountedPrice() {
        return price * (1.0 - getDiscountRate());
    }
    
    /**
     * Hook method for discount rate. Default is no discount.
     * Subclasses can override to provide type-specific discounts.
     * 
     * @return discount rate between 0.0 and 1.0
     */
    public double getDiscountRate() {
        return 0.0;
    }
    
    protected String validateId(String id) {
        if (id == null) {
            throw new NullPointerException("ID cannot be null");
        }
        if (id.trim().isEmpty()) {
            throw new IllegalArgumentException("ID cannot be blank");
        }
        return id.trim();
    }
    
    protected String validateStringField(String value, String fieldName) {
        if (value == null) {
            throw new NullPointerException(fieldName + " cannot be null");
        }
        if (value.trim().isEmpty()) {
            throw new IllegalArgumentException(fieldName + " cannot be blank");
        }
        return value.trim();
    }
    
    protected double validatePrice(double price) {
        if (price < 0.0) {
            throw new IllegalArgumentException(
                "Price cannot be negative. Provided: " + price);
        }
        if (Double.isNaN(price) || Double.isInfinite(price)) {
            throw new IllegalArgumentException(
                "Price must be a valid number. Provided: " + price);
        }
        return price;
    }
    
    protected int validateYear(int year) {
        int currentYear = Year.now().getValue();
        if (year < MIN_YEAR || year > currentYear + 1) {
            throw new IllegalArgumentException(
                String.format("Year must be between %d and %d. Provided: %d",
                    MIN_YEAR, currentYear + 1, year));
        }
        return year;
    }
    
    public String getId() {
        return id;
    }
    
    public String getTitle() {
        return title;
    }
    
    public double getPrice() {
        return price;
    }
    
    public int getYear() {
        return year;
    }
    
    public MaterialType getType() {
        return type;
    }
    
    /**
     * Compares materials by title for natural ordering.
     */
    @Override
    public int compareTo(Material other) {
        if (other == null) {
            throw new NullPointerException("Cannot compare to null Material");
        }
        int titleComparison = this.title.compareToIgnoreCase(other.title);
        if (titleComparison != 0) {
            return titleComparison;
        }
        return this.id.compareTo(other.id);
    }
    
    /**
     * Materials are equal if they have the same ID.
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Material)) return false;
        Material other = (Material) obj;
        return id.equals(other.id);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
    
    @Override
    public String toString() {
        return String.format("%s[ID=%s, Title='%s', Price=$%.2f, Year=%d]",
            type.getDisplayName(), id, title, price, year);
    }
}