MaterialBundle.java

package com.university.bookstore.composite;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import com.university.bookstore.model.Material;

/**
 * Composite component in the Composite pattern.
 * Represents a bundle of materials that can contain other components (both leaves and other bundles).
 * 
 * <p>This class implements the MaterialComponent interface and can contain other MaterialComponent
 * objects, enabling recursive composition and unified treatment of individual materials and bundles.</p>
 * 
 * @author Navid Mohaghegh
 * @version 3.0
 * @since 2024-09-15
 */
public class MaterialBundle implements MaterialComponent {
    
    private final String bundleName;
    private final List<MaterialComponent> components;
    private final double bundleDiscount;
    
    /**
     * Creates a new material bundle with the specified name and discount.
     * 
     * @param bundleName the name of the bundle
     * @param bundleDiscount the discount rate (0.0 to 1.0)
     * @throws IllegalArgumentException if bundleName is null or empty, or discount is invalid
     */
    public MaterialBundle(String bundleName, double bundleDiscount) {
        if (bundleName == null || bundleName.trim().isEmpty()) {
            throw new IllegalArgumentException("Bundle name cannot be null or empty");
        }
        
        this.bundleName = bundleName.trim();
        this.components = new ArrayList<>();
        this.bundleDiscount = Math.max(0.0, Math.min(1.0, bundleDiscount));
    }
    
    /**
     * Adds a component to this bundle.
     * 
     * @param component the component to add
     * @throws IllegalArgumentException if component is null
     */
    public void addComponent(MaterialComponent component) {
        if (component == null) {
            throw new IllegalArgumentException("Component cannot be null");
        }
        
        // Prevent adding a bundle to itself (directly or indirectly)
        if (component == this || (component instanceof MaterialBundle && containsBundle((MaterialBundle) component))) {
            throw new IllegalArgumentException("Cannot add bundle to itself or create circular references");
        }
        
        components.add(component);
    }
    
    /**
     * Removes a component from this bundle.
     * 
     * @param component the component to remove
     * @return true if the component was removed, false if not found
     */
    public boolean removeComponent(MaterialComponent component) {
        return components.remove(component);
    }
    
    /**
     * Gets all components in this bundle.
     * 
     * @return list of components (defensive copy)
     */
    public List<MaterialComponent> getComponents() {
        return new ArrayList<>(components);
    }
    
    /**
     * Gets the number of components in this bundle.
     * 
     * @return the component count
     */
    public int getComponentCount() {
        return components.size();
    }
    
    @Override
    public String getTitle() {
        return bundleName;
    }
    
    @Override
    public double getPrice() {
        return components.stream()
            .mapToDouble(MaterialComponent::getPrice)
            .sum();
    }
    
    @Override
    public double getDiscountedPrice() {
        double totalPrice = getPrice();
        return totalPrice * (1.0 - bundleDiscount);
    }
    
    @Override
    public String getDescription() {
        StringBuilder desc = new StringBuilder();
        desc.append("Bundle: ").append(bundleName)
            .append(" (").append(getItemCount()).append(" items, ")
            .append(String.format("%.1f%% discount", bundleDiscount * 100))
            .append(")\n");
        
        for (MaterialComponent component : components) {
            desc.append("  - ").append(component.getDescription()).append("\n");
        }
        
        return desc.toString();
    }
    
    @Override
    public List<Material> getMaterials() {
        return components.stream()
            .flatMap(component -> component.getMaterials().stream())
            .collect(Collectors.toList());
    }
    
    @Override
    public int getItemCount() {
        return components.stream()
            .mapToInt(MaterialComponent::getItemCount)
            .sum();
    }
    
    @Override
    public double getDiscountRate() {
        return bundleDiscount;
    }
    
    @Override
    public boolean isLeaf() {
        return false;
    }
    
    /**
     * Gets the bundle name.
     * 
     * @return the bundle name
     */
    public String getBundleName() {
        return bundleName;
    }
    
    /**
     * Gets the bundle discount rate.
     * 
     * @return the discount rate
     */
    public double getBundleDiscount() {
        return bundleDiscount;
    }
    
    /**
     * Calculates the total savings from the bundle discount.
     * 
     * @return the total savings amount
     */
    public double getTotalSavings() {
        return getPrice() - getDiscountedPrice();
    }
    
    /**
     * Checks if this bundle contains any materials of the specified type.
     * 
     * @param type the material type to check for
     * @return true if the bundle contains materials of the specified type
     */
    public boolean containsType(Material.MaterialType type) {
        return getMaterials().stream()
            .anyMatch(material -> material.getType() == type);
    }
    
    /**
     * Gets all materials of the specified type in this bundle.
     * 
     * @param type the material type to filter by
     * @return list of materials of the specified type
     */
    public List<Material> getMaterialsByType(Material.MaterialType type) {
        return getMaterials().stream()
            .filter(material -> material.getType() == type)
            .collect(Collectors.toList());
    }
    
    /**
     * Checks if this bundle contains the specified material.
     * 
     * @param material the material to check for
     * @return true if the bundle contains the material
     */
    public boolean containsMaterial(Material material) {
        return getMaterials().contains(material);
    }
    
    /**
     * Checks if this bundle contains the specified bundle (recursively).
     * 
     * @param bundle the bundle to check for
     * @return true if this bundle contains the specified bundle
     */
    private boolean containsBundle(MaterialBundle bundle) {
        for (MaterialComponent component : components) {
            if (component == bundle) {
                return true;
            }
            if (component instanceof MaterialBundle) {
                MaterialBundle childBundle = (MaterialBundle) component;
                if (childBundle.containsBundle(bundle)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        
        MaterialBundle that = (MaterialBundle) obj;
        return Objects.equals(bundleName, that.bundleName) &&
               Double.compare(that.bundleDiscount, bundleDiscount) == 0 &&
               Objects.equals(components, that.components);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(bundleName, bundleDiscount, components);
    }
    
    @Override
    public String toString() {
        return String.format("MaterialBundle[%s, %d items, %.1f%% discount, $%.2f]",
            bundleName, getItemCount(), bundleDiscount * 100, getDiscountedPrice());
    }
}