Book.java

package com.university.bookstore.model;

import java.time.Year;
import java.util.Objects;
import java.util.regex.Pattern;

/**
 * Represents an immutable book in the bookstore inventory.
 * Books are uniquely identified by their ISBN.
 * 
 * <p>This class is immutable and thread-safe. All fields are validated
 * during construction to ensure data integrity.</p>
 * 
 * @author Navid Mohaghegh
 * @version 1.0
 * @since 2024-09-15
 */
public final class Book implements Comparable<Book> {
    
    private static final Pattern ISBN_13_PATTERN = Pattern.compile("^\\d{13}$");
    private static final Pattern ISBN_10_PATTERN = Pattern.compile("^\\d{9}[\\dX]$");
    private static final int MIN_YEAR = 1450; // Invention of printing press
    
    private final String isbn;
    private final String title;
    private final String author;
    private final double price;
    private final int year;
    
    /**
     * Creates a new Book with validation.
     * 
     * @param isbn the International Standard Book Number (10 or 13 digits)
     * @param title the book title (non-null, non-blank)
     * @param author the primary author (non-null, non-blank)
     * @param price the price in dollars (non-negative)
     * @param year the publication year (1450 to current year + 1)
     * @throws IllegalArgumentException if any parameter is invalid
     * @throws NullPointerException if any string parameter is null
     */
    public Book(String isbn, String title, String author, double price, int year) {
        this.isbn = validateIsbn(isbn);
        this.title = validateStringField(title, "Title");
        this.author = validateStringField(author, "Author");
        this.price = validatePrice(price);
        this.year = validateYear(year);
    }
    
    private String validateIsbn(String isbn) {
        if (isbn == null) {
            throw new NullPointerException("ISBN cannot be null");
        }
        
        String cleaned = isbn.replaceAll("-", "").trim();
        
        if (!ISBN_10_PATTERN.matcher(cleaned).matches() && 
            !ISBN_13_PATTERN.matcher(cleaned).matches()) {
            throw new IllegalArgumentException(
                "ISBN must be 10 or 13 digits. Provided: " + isbn);
        }
        
        return cleaned;
    }
    
    private 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();
    }
    
    private 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;
    }
    
    private 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;
    }
    
    /**
     * Gets the ISBN of this book.
     * @return the ISBN (10 or 13 digits)
     */
    public String getIsbn() {
        return isbn;
    }
    
    /**
     * Gets the title of this book.
     * @return the book title
     */
    public String getTitle() {
        return title;
    }
    
    /**
     * Gets the author of this book.
     * @return the author name
     */
    public String getAuthor() {
        return author;
    }
    
    /**
     * Gets the price of this book.
     * @return the price in dollars
     */
    public double getPrice() {
        return price;
    }
    
    /**
     * Gets the publication year of this book.
     * @return the publication year
     */
    public int getYear() {
        return year;
    }
    
    /**
     * Compares this book with another book based on title (alphabetical order).
     * 
     * @param other the book to compare with
     * @return negative if this book comes before, positive if after, 0 if equal
     */
    @Override
    public int compareTo(Book other) {
        if (other == null) {
            throw new NullPointerException("Cannot compare to null Book");
        }
        return this.title.compareToIgnoreCase(other.title);
    }
    
    /**
     * Checks if this book is equal to another object.
     * Books are considered equal if they have the same ISBN.
     * 
     * @param obj the object to compare with
     * @return true if the objects are equal (same ISBN), false otherwise
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Book)) return false;
        Book other = (Book) obj;
        return isbn.equals(other.isbn);
    }
    
    /**
     * Generates hash code based on ISBN.
     * 
     * @return hash code of the ISBN
     */
    @Override
    public int hashCode() {
        return Objects.hash(isbn);
    }
    
    /**
     * Returns a human-readable string representation of this book.
     * 
     * @return formatted string with book details
     */
    @Override
    public String toString() {
        return String.format("Book[ISBN=%s, Title='%s', Author='%s', Price=$%.2f, Year=%d]",
            isbn, title, author, price, year);
    }
}