BundleService.java
package com.university.bookstore.composite;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.university.bookstore.model.Material;
/**
* Service for managing material bundles using the Composite pattern.
* Provides operations for creating, managing, and analyzing material bundles.
*
* <p>This service demonstrates the Composite pattern in action by providing
* a high-level interface for working with both individual materials and bundles.</p>
*
* @author Navid Mohaghegh
* @version 3.0
* @since 2024-09-15
*/
public class BundleService {
private final Map<String, MaterialBundle> bundles;
/**
* Creates a new bundle service.
*/
public BundleService() {
this.bundles = new HashMap<>();
}
/**
* Creates a new material bundle with the specified name and discount.
*
* @param name the bundle name
* @param discount the discount rate (0.0 to 1.0)
* @return the created bundle
* @throws IllegalArgumentException if name is null or empty
*/
public MaterialBundle createBundle(String name, double discount) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Bundle name cannot be null or empty");
}
String bundleName = name.trim();
if (bundles.containsKey(bundleName)) {
throw new IllegalArgumentException("Bundle already exists: " + bundleName);
}
MaterialBundle bundle = new MaterialBundle(bundleName, discount);
bundles.put(bundleName, bundle);
return bundle;
}
/**
* Adds a material to the specified bundle.
*
* @param bundleName the name of the bundle
* @param material the material to add
* @throws IllegalArgumentException if bundle not found or material is null
*/
public void addToBundle(String bundleName, Material material) {
if (material == null) {
throw new IllegalArgumentException("Material cannot be null");
}
MaterialBundle bundle = bundles.get(bundleName);
if (bundle == null) {
throw new IllegalArgumentException("Bundle not found: " + bundleName);
}
bundle.addComponent(new MaterialLeaf(material));
}
/**
* Adds a bundle to another bundle (nested bundles).
*
* @param parentBundleName the name of the parent bundle
* @param childBundleName the name of the child bundle to add
* @throws IllegalArgumentException if either bundle is not found
*/
public void addBundleToBundle(String parentBundleName, String childBundleName) {
MaterialBundle parent = bundles.get(parentBundleName);
MaterialBundle child = bundles.get(childBundleName);
if (parent == null) {
throw new IllegalArgumentException("Parent bundle not found: " + parentBundleName);
}
if (child == null) {
throw new IllegalArgumentException("Child bundle not found: " + childBundleName);
}
parent.addComponent(child);
}
/**
* Adds a component to the specified bundle.
*
* @param bundleName the name of the bundle
* @param component the component to add
* @throws IllegalArgumentException if bundle not found or component is null
*/
public void addComponentToBundle(String bundleName, MaterialComponent component) {
if (component == null) {
throw new IllegalArgumentException("Component cannot be null");
}
MaterialBundle bundle = bundles.get(bundleName);
if (bundle == null) {
throw new IllegalArgumentException("Bundle not found: " + bundleName);
}
bundle.addComponent(component);
}
/**
* Removes a material from the specified bundle.
*
* @param bundleName the name of the bundle
* @param material the material to remove
* @return true if the material was removed, false if not found
*/
public boolean removeFromBundle(String bundleName, Material material) {
MaterialBundle bundle = bundles.get(bundleName);
if (bundle == null) {
return false;
}
return bundle.removeComponent(new MaterialLeaf(material));
}
/**
* Gets a bundle by name.
*
* @param name the bundle name
* @return Optional containing the bundle if found
*/
public Optional<MaterialBundle> getBundle(String name) {
return Optional.ofNullable(bundles.get(name));
}
/**
* Gets all bundles.
*
* @return list of all bundles
*/
public List<MaterialBundle> getAllBundles() {
return new ArrayList<>(bundles.values());
}
/**
* Gets all bundle names.
*
* @return list of bundle names
*/
public List<String> getBundleNames() {
return new ArrayList<>(bundles.keySet());
}
/**
* Calculates the total savings for a bundle.
*
* @param bundleName the name of the bundle
* @return the total savings amount
* @throws IllegalArgumentException if bundle not found
*/
public double calculateBundleSavings(String bundleName) {
MaterialBundle bundle = bundles.get(bundleName);
if (bundle == null) {
throw new IllegalArgumentException("Bundle not found: " + bundleName);
}
return bundle.getTotalSavings();
}
/**
* Calculates the total savings across all bundles.
*
* @return the total savings amount
*/
public double calculateTotalSavings() {
return bundles.values().stream()
.mapToDouble(MaterialBundle::getTotalSavings)
.sum();
}
/**
* Gets the total value of all bundles.
*
* @return the total value
*/
public double getTotalBundleValue() {
return bundles.values().stream()
.mapToDouble(MaterialBundle::getPrice)
.sum();
}
/**
* Gets the total discounted value of all bundles.
*
* @return the total discounted value
*/
public double getTotalDiscountedBundleValue() {
return bundles.values().stream()
.mapToDouble(MaterialBundle::getDiscountedPrice)
.sum();
}
/**
* Gets bundles that contain materials of the specified type.
*
* @param type the material type to filter by
* @return list of bundles containing the specified type
*/
public List<MaterialBundle> getBundlesByMaterialType(Material.MaterialType type) {
return bundles.values().stream()
.filter(bundle -> bundle.containsType(type))
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
/**
* Gets bundles with discounts greater than or equal to the specified threshold.
*
* @param minDiscount the minimum discount rate
* @return list of bundles meeting the criteria
*/
public List<MaterialBundle> getBundlesByDiscount(double minDiscount) {
return bundles.values().stream()
.filter(bundle -> bundle.getDiscountRate() >= minDiscount)
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
/**
* Gets bundles with values within the specified range.
*
* @param minValue the minimum value
* @param maxValue the maximum value
* @return list of bundles within the value range
*/
public List<MaterialBundle> getBundlesByValueRange(double minValue, double maxValue) {
return bundles.values().stream()
.filter(bundle -> {
double value = bundle.getPrice();
return value >= minValue && value <= maxValue;
})
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
/**
* Removes a bundle by name.
*
* @param name the bundle name
* @return true if the bundle was removed, false if not found
*/
public boolean removeBundle(String name) {
return bundles.remove(name) != null;
}
/**
* Clears all bundles.
*/
public void clearAllBundles() {
bundles.clear();
}
/**
* Gets the number of bundles.
*
* @return the bundle count
*/
public int getBundleCount() {
return bundles.size();
}
/**
* Checks if any bundles exist.
*
* @return true if no bundles exist
*/
public boolean isEmpty() {
return bundles.isEmpty();
}
/**
* Gets bundle statistics.
*
* @return bundle statistics
*/
public BundleStats getBundleStats() {
int totalBundles = bundles.size();
int totalItems = bundles.values().stream()
.mapToInt(MaterialBundle::getItemCount)
.sum();
double totalValue = getTotalBundleValue();
double totalDiscountedValue = getTotalDiscountedBundleValue();
double totalSavings = calculateTotalSavings();
double averageDiscount = bundles.values().stream()
.mapToDouble(MaterialBundle::getDiscountRate)
.average()
.orElse(0.0);
return new BundleStats(
totalBundles,
totalItems,
totalValue,
totalDiscountedValue,
totalSavings,
averageDiscount
);
}
/**
* Statistics class for bundle analysis.
*/
public static class BundleStats {
private final int totalBundles;
private final int totalItems;
private final double totalValue;
private final double totalDiscountedValue;
private final double totalSavings;
private final double averageDiscount;
public BundleStats(int totalBundles, int totalItems, double totalValue,
double totalDiscountedValue, double totalSavings, double averageDiscount) {
this.totalBundles = totalBundles;
this.totalItems = totalItems;
this.totalValue = totalValue;
this.totalDiscountedValue = totalDiscountedValue;
this.totalSavings = totalSavings;
this.averageDiscount = averageDiscount;
}
public int getTotalBundles() { return totalBundles; }
public int getTotalItems() { return totalItems; }
public double getTotalValue() { return totalValue; }
public double getTotalDiscountedValue() { return totalDiscountedValue; }
public double getTotalSavings() { return totalSavings; }
public double getAverageDiscount() { return averageDiscount; }
@Override
public String toString() {
return String.format("BundleStats[Bundles=%d, Items=%d, Value=$%.2f, Discounted=$%.2f, Savings=$%.2f, AvgDiscount=%.1f%%]",
totalBundles, totalItems, totalValue, totalDiscountedValue, totalSavings, averageDiscount * 100);
}
}
@Override
public String toString() {
return String.format("BundleService[Bundles=%d, TotalValue=$%.2f, TotalSavings=$%.2f]",
getBundleCount(), getTotalBundleValue(), calculateTotalSavings());
}
}