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