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