← Back to Lecture
Practical Work 7

Library Management System - Loan Management

Implement loan tracking with dates, receipts, and reports using modern Java APIs

Duration 2 hours
Difficulty Intermediate
Session 7 - Date/Time API & Strings

Objectives

By the end of this practical work, you will be able to:

  • Use LocalDate and LocalDateTime for date operations
  • Calculate due dates and check for overdue items with Period
  • Format dates for display using DateTimeFormatter
  • Build formatted output with StringBuilder
  • Use text blocks for multi-line templates
  • Generate professional loan receipts and reports

Prerequisites

  • Completed Practical Work 6
  • MediaItem hierarchy with Book, DVD, Magazine
  • Borrowable interface implemented
  • LoanInfo record created

Project Structure

Update your project with loan management services:

library-system
src
com.library
model
MediaItem.java
Book.java
LoanInfo.java
Loan.java
Member.java
service
LoanService.java
ReceiptGenerator.java
ReportGenerator.java
MediaCatalog.java
app
LibraryApp.java

Part 1: Loan Class with Date Management

Step 1.1: Create Loan Class

Create a class to manage individual loans with date tracking:

package com.library.model;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;

public class Loan {
    private final String loanId;
    private final MediaItem item;
    private final Member member;
    private final LocalDateTime checkoutDateTime;  // (#1:Precise checkout time)
    private final LocalDate dueDate;
    private LocalDateTime returnDateTime;
    private double lateFeeCharged;

    // Standard date formatters
    public static final DateTimeFormatter DATE_FORMAT =
        DateTimeFormatter.ofPattern("dd/MM/yyyy");  // (#2:Custom pattern)
    public static final DateTimeFormatter DATETIME_FORMAT =
        DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");

    public Loan(String loanId, MediaItem item, Member member, int loanDays) {
        this.loanId = loanId;
        this.item = item;
        this.member = member;
        this.checkoutDateTime = LocalDateTime.now();  // (#3:Current date-time)
        this.dueDate = LocalDate.now().plusDays(loanDays);  // (#4:Calculate due date)
        this.lateFeeCharged = 0.0;
    }

    // Check if loan is overdue
    public boolean isOverdue() {
        if (returnDateTime != null) {
            return returnDateTime.toLocalDate().isAfter(dueDate);  // (#5:Compare dates)
        }
        return LocalDate.now().isAfter(dueDate);
    }

    // Calculate days overdue
    public long getDaysOverdue() {
        LocalDate checkDate = (returnDateTime != null)
            ? returnDateTime.toLocalDate()
            : LocalDate.now();

        if (checkDate.isAfter(dueDate)) {
            return ChronoUnit.DAYS.between(dueDate, checkDate);  // (#6:Days between)
        }
        return 0;
    }

    // Calculate days until due (negative if overdue)
    public long getDaysUntilDue() {
        return ChronoUnit.DAYS.between(LocalDate.now(), dueDate);
    }

    // Get loan duration using Period
    public Period getLoanDuration() {
        LocalDate endDate = (returnDateTime != null)
            ? returnDateTime.toLocalDate()
            : LocalDate.now();
        return Period.between(checkoutDateTime.toLocalDate(), endDate);  // (#7:Period calculation)
    }

    // Return the item
    public void returnItem() {
        this.returnDateTime = LocalDateTime.now();
        if (isOverdue()) {
            this.lateFeeCharged = getDaysOverdue() * item.getLateFeePerDay();
        }
    }

    // Getters with formatted dates
    public String getFormattedCheckoutDate() {
        return checkoutDateTime.format(DATE_FORMAT);
    }

    public String getFormattedDueDate() {
        return dueDate.format(DATE_FORMAT);
    }

    public String getFormattedReturnDate() {
        return (returnDateTime != null)
            ? returnDateTime.format(DATETIME_FORMAT)
            : "Not returned";
    }

    // Standard getters
    public String getLoanId() { return loanId; }
    public MediaItem getItem() { return item; }
    public Member getMember() { return member; }
    public LocalDateTime getCheckoutDateTime() { return checkoutDateTime; }
    public LocalDate getDueDate() { return dueDate; }
    public LocalDateTime getReturnDateTime() { return returnDateTime; }
    public double getLateFeeCharged() { return lateFeeCharged; }
    public boolean isReturned() { return returnDateTime != null; }
}
Code Annotations:
  1. LocalDateTime: Captures exact moment (date + time)
  2. DateTimeFormatter: Custom format pattern for display
  3. now(): Get current date-time
  4. plusDays(): Add days to calculate due date
  5. isAfter(): Compare two dates
  6. ChronoUnit.DAYS.between(): Calculate exact days between dates
  7. Period: Represents duration in years/months/days

Part 2: Loan Service

Step 2.1: Create LoanService Class

Manage all loan operations with date validation:

package com.library.service;

import com.library.model.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;

public class LoanService {
    private Loan[] activeLoans;
    private Loan[] loanHistory;
    private int activeCount;
    private int historyCount;
    private int loanIdCounter;

    public LoanService() {
        activeLoans = new Loan[50];
        loanHistory = new Loan[200];
        activeCount = 0;
        historyCount = 0;
        loanIdCounter = 1;
    }

    // Create a new loan
    public Loan checkoutItem(MediaItem item, Member member) {
        // Validate item can be borrowed
        if (!item.getStatus().canBeBorrowed()) {
            throw new IllegalStateException(
                "Item cannot be borrowed: " + item.getStatus().getDescription());
        }

        // Create loan with item-specific duration
        String loanId = generateLoanId();
        Loan loan = new Loan(loanId, item, member, item.getMaxLoanDays());

        // Update item status
        item.setStatus(MediaStatus.BORROWED);

        // Store loan
        if (activeCount >= activeLoans.length) {
            activeLoans = Arrays.copyOf(activeLoans, activeLoans.length * 2);
        }
        activeLoans[activeCount++] = loan;

        return loan;
    }

    // Return an item
    public Loan returnItem(String loanId) {
        for (int i = 0; i < activeCount; i++) {
            if (activeLoans[i].getLoanId().equals(loanId)) {
                Loan loan = activeLoans[i];
                loan.returnItem();
                loan.getItem().setStatus(MediaStatus.AVAILABLE);

                // Move to history
                addToHistory(loan);

                // Remove from active loans
                removeFromActive(i);

                return loan;
            }
        }
        throw new IllegalArgumentException("Loan not found: " + loanId);
    }

    // Find overdue loans
    public Loan[] getOverdueLoans() {
        int count = 0;
        for (int i = 0; i < activeCount; i++) {
            if (activeLoans[i].isOverdue()) count++;
        }

        Loan[] overdue = new Loan[count];
        int index = 0;
        for (int i = 0; i < activeCount; i++) {
            if (activeLoans[i].isOverdue()) {
                overdue[index++] = activeLoans[i];
            }
        }
        return overdue;
    }

    // Find loans due soon (within specified days)
    public Loan[] getLoansDueSoon(int withinDays) {
        int count = 0;
        for (int i = 0; i < activeCount; i++) {
            long daysUntilDue = activeLoans[i].getDaysUntilDue();
            if (daysUntilDue >= 0 && daysUntilDue <= withinDays) {
                count++;
            }
        }

        Loan[] dueSoon = new Loan[count];
        int index = 0;
        for (int i = 0; i < activeCount; i++) {
            long daysUntilDue = activeLoans[i].getDaysUntilDue();
            if (daysUntilDue >= 0 && daysUntilDue <= withinDays) {
                dueSoon[index++] = activeLoans[i];
            }
        }
        return dueSoon;
    }

    // Parse date from user input with validation
    public static LocalDate parseDate(String input) {  // (#1:Static utility method)
        try {
            return LocalDate.parse(input, DateTimeFormatter.ISO_LOCAL_DATE);
        } catch (DateTimeParseException e) {
            throw new IllegalArgumentException(
                "Invalid date format. Please use yyyy-MM-dd (e.g., 2025-01-15)");
        }
    }

    // Calculate late fee for a loan
    public double calculateLateFee(Loan loan) {
        if (!loan.isOverdue()) return 0.0;
        return loan.getDaysOverdue() * loan.getItem().getLateFeePerDay();
    }

    // Get total outstanding late fees
    public double getTotalOutstandingFees() {
        double total = 0.0;
        for (int i = 0; i < activeCount; i++) {
            total += calculateLateFee(activeLoans[i]);
        }
        return total;
    }

    // Helper methods
    private String generateLoanId() {
        return String.format("L%05d", loanIdCounter++);  // (#2:Formatted ID)
    }

    private void addToHistory(Loan loan) {
        if (historyCount >= loanHistory.length) {
            loanHistory = Arrays.copyOf(loanHistory, loanHistory.length * 2);
        }
        loanHistory[historyCount++] = loan;
    }

    private void removeFromActive(int index) {
        for (int i = index; i < activeCount - 1; i++) {
            activeLoans[i] = activeLoans[i + 1];
        }
        activeLoans[--activeCount] = null;
    }

    // Getters
    public Loan[] getActiveLoans() {
        return Arrays.copyOf(activeLoans, activeCount);
    }

    public int getActiveCount() { return activeCount; }
}
Code Annotations:
  1. Static utility: Parse date without instance, handles exceptions
  2. String.format: Create formatted loan ID with leading zeros

Part 3: Receipt Generation with StringBuilder

Step 3.1: Create ReceiptGenerator Class

Generate formatted loan receipts using StringBuilder:

package com.library.service;

import com.library.model.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class ReceiptGenerator {
    private static final int RECEIPT_WIDTH = 45;
    private static final String LIBRARY_NAME = "City Central Library";
    private static final String LIBRARY_ADDRESS = "123 Main Street";

    private static final DateTimeFormatter RECEIPT_DATE_FORMAT =
        DateTimeFormatter.ofPattern("dd MMM yyyy");
    private static final DateTimeFormatter RECEIPT_TIME_FORMAT =
        DateTimeFormatter.ofPattern("HH:mm:ss");

    // Generate checkout receipt
    public String generateCheckoutReceipt(Loan loan) {
        StringBuilder sb = new StringBuilder();  // (#1:StringBuilder for building)

        // Header
        sb.append(centerText("=" .repeat(RECEIPT_WIDTH))).append("\n");
        sb.append(centerText(LIBRARY_NAME)).append("\n");
        sb.append(centerText(LIBRARY_ADDRESS)).append("\n");
        sb.append(centerText("=" .repeat(RECEIPT_WIDTH))).append("\n\n");

        // Transaction info
        sb.append("CHECKOUT RECEIPT\n");
        sb.append("-".repeat(RECEIPT_WIDTH)).append("\n");

        LocalDateTime now = LocalDateTime.now();
        sb.append(String.format("Date: %s%n", now.format(RECEIPT_DATE_FORMAT)));
        sb.append(String.format("Time: %s%n", now.format(RECEIPT_TIME_FORMAT)));
        sb.append(String.format("Loan ID: %s%n", loan.getLoanId()));
        sb.append("\n");

        // Member info
        sb.append("MEMBER\n");
        sb.append(String.format("  Name: %s%n", loan.getMember().getName()));
        sb.append(String.format("  ID: %s%n", loan.getMember().getId()));
        sb.append("\n");

        // Item info
        sb.append("ITEM BORROWED\n");
        sb.append(String.format("  Type: %s%n", loan.getItem().getMediaType()));
        sb.append(String.format("  Title: %s%n", loan.getItem().getTitle()));
        sb.append(String.format("  ID: %s%n", loan.getItem().getId()));
        sb.append("\n");

        // Due date (important!)
        sb.append("-".repeat(RECEIPT_WIDTH)).append("\n");
        sb.append(String.format("DUE DATE: %s%n", loan.getFormattedDueDate()));  // (#2:Formatted date)
        sb.append(String.format("Loan period: %d days%n", loan.getItem().getMaxLoanDays()));
        sb.append(String.format("Late fee: $%.2f per day%n", loan.getItem().getLateFeePerDay()));
        sb.append("-".repeat(RECEIPT_WIDTH)).append("\n\n");

        // Footer
        sb.append(centerText("Thank you for using our library!")).append("\n");
        sb.append(centerText("Please return items on time.")).append("\n");
        sb.append(centerText("=" .repeat(RECEIPT_WIDTH))).append("\n");

        return sb.toString();
    }

    // Generate return receipt
    public String generateReturnReceipt(Loan loan) {
        StringBuilder sb = new StringBuilder();

        // Header
        sb.append(centerText("=" .repeat(RECEIPT_WIDTH))).append("\n");
        sb.append(centerText(LIBRARY_NAME)).append("\n");
        sb.append(centerText("RETURN RECEIPT")).append("\n");
        sb.append(centerText("=" .repeat(RECEIPT_WIDTH))).append("\n\n");

        // Transaction summary
        sb.append(String.format("Loan ID: %s%n", loan.getLoanId()));
        sb.append(String.format("Item: %s%n", loan.getItem().getTitle()));
        sb.append(String.format("Member: %s%n", loan.getMember().getName()));
        sb.append("\n");

        // Date summary
        sb.append("DATE SUMMARY\n");
        sb.append(String.format("  Checked out: %s%n", loan.getFormattedCheckoutDate()));
        sb.append(String.format("  Due date: %s%n", loan.getFormattedDueDate()));
        sb.append(String.format("  Returned: %s%n", loan.getFormattedReturnDate()));
        sb.append("\n");

        // Duration info
        var duration = loan.getLoanDuration();  // (#3:Period object)
        sb.append(String.format("Loan duration: %d days%n",
            duration.getDays() + duration.getMonths() * 30 + duration.getYears() * 365));

        // Late fee if applicable
        if (loan.isOverdue()) {
            sb.append("\n");
            sb.append("-".repeat(RECEIPT_WIDTH)).append("\n");
            sb.append(String.format("LATE BY: %d days%n", loan.getDaysOverdue()));
            sb.append(String.format("LATE FEE: $%.2f%n", loan.getLateFeeCharged()));  // (#4:Fee display)
            sb.append("-".repeat(RECEIPT_WIDTH)).append("\n");
        } else {
            sb.append("\n");
            sb.append(centerText("Returned on time - Thank you!")).append("\n");
        }

        sb.append("\n");
        sb.append(centerText("=" .repeat(RECEIPT_WIDTH))).append("\n");

        return sb.toString();
    }

    // Helper to center text
    private String centerText(String text) {
        if (text.length() >= RECEIPT_WIDTH) return text;
        int padding = (RECEIPT_WIDTH - text.length()) / 2;
        return " ".repeat(padding) + text;
    }
}
Code Annotations:
  1. StringBuilder: Efficiently builds string without creating many intermediate objects
  2. Formatted dates: User-friendly date format for receipts
  3. Period: Duration of the loan in date terms
  4. Currency format: %.2f shows 2 decimal places

Part 4: Report Generation with Text Blocks

Step 4.1: Create ReportGenerator Class

Generate reports using text blocks for templates:

package com.library.service;

import com.library.model.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class ReportGenerator {
    private final LoanService loanService;
    private static final DateTimeFormatter REPORT_DATE =
        DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy");

    public ReportGenerator(LoanService loanService) {
        this.loanService = loanService;
    }

    // Daily summary report using text blocks
    public String generateDailySummary() {
        Loan[] active = loanService.getActiveLoans();
        Loan[] overdue = loanService.getOverdueLoans();
        Loan[] dueSoon = loanService.getLoansDueSoon(3);

        // Text block for report template  (#1:Text block with placeholders)
        String report = """
            ╔══════════════════════════════════════════════════════════════╗
            ║                    DAILY LIBRARY REPORT                      ║
            ╠══════════════════════════════════════════════════════════════╣
            ║  Date: %-52s ║
            ╠══════════════════════════════════════════════════════════════╣
            ║  LOAN STATISTICS                                             ║
            ║  ─────────────────                                           ║
            ║  Active Loans:        %3d                                    ║
            ║  Overdue Items:       %3d                                    ║
            ║  Due Within 3 Days:   %3d                                    ║
            ║  Outstanding Fees:    $%7.2f                                 ║
            ╚══════════════════════════════════════════════════════════════╝
            """.formatted(  // (#2:formatted() method on text block)
                LocalDate.now().format(REPORT_DATE),
                active.length,
                overdue.length,
                dueSoon.length,
                loanService.getTotalOutstandingFees()
            );

        return report;
    }

    // Overdue items report
    public String generateOverdueReport() {
        Loan[] overdue = loanService.getOverdueLoans();

        StringBuilder sb = new StringBuilder();

        // Header using text block
        sb.append("""
            ┌──────────────────────────────────────────────────────────────┐
            │                    OVERDUE ITEMS REPORT                      │
            │                    Generated: %s                     │
            └──────────────────────────────────────────────────────────────┘

            """.formatted(LocalDateTime.now().format(
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))));

        if (overdue.length == 0) {
            sb.append("No overdue items. Great job!\n");
        } else {
            sb.append(String.format("Total overdue items: %d%n%n", overdue.length));

            // Table header
            sb.append(String.format("%-10s %-25s %-15s %-8s %-10s%n",
                "Loan ID", "Title", "Member", "Days", "Fee"));
            sb.append("-".repeat(70)).append("\n");

            // Table rows
            double totalFees = 0;
            for (Loan loan : overdue) {
                double fee = loanService.calculateLateFee(loan);
                totalFees += fee;
                sb.append(String.format("%-10s %-25s %-15s %5d    $%7.2f%n",
                    loan.getLoanId(),
                    truncate(loan.getItem().getTitle(), 25),  // (#3:Truncate long titles)
                    truncate(loan.getMember().getName(), 15),
                    loan.getDaysOverdue(),
                    fee));
            }

            sb.append("-".repeat(70)).append("\n");
            sb.append(String.format("%60s $%7.2f%n", "Total Outstanding:", totalFees));
        }

        return sb.toString();
    }

    // Member loan history report
    public String generateMemberReport(Member member, Loan[] memberLoans) {
        // Using text block with escape for quotes  (#4:Escape sequences in text blocks)
        String header = """
            ┌──────────────────────────────────────────────────────────────┐
            │                    MEMBER LOAN REPORT                        │
            ├──────────────────────────────────────────────────────────────┤
            │  Member: %-51s │
            │  ID: %-55s │
            │  Report Date: %-46s │
            └──────────────────────────────────────────────────────────────┘

            """.formatted(
                member.getName(),
                member.getId(),
                LocalDate.now().format(REPORT_DATE)
            );

        StringBuilder sb = new StringBuilder(header);

        // Current loans section
        sb.append("CURRENT LOANS\n");
        sb.append("─".repeat(40)).append("\n");

        int currentCount = 0;
        for (Loan loan : memberLoans) {
            if (!loan.isReturned()) {
                currentCount++;
                String status = loan.isOverdue() ? "OVERDUE" : "Active";
                sb.append(String.format("  • %s - Due: %s [%s]%n",
                    loan.getItem().getTitle(),
                    loan.getFormattedDueDate(),
                    status));
            }
        }

        if (currentCount == 0) {
            sb.append("  No current loans\n");
        }

        sb.append("\n");
        return sb.toString();
    }

    // Upcoming due dates report (next 7 days)
    public String generateUpcomingDueReport() {
        StringBuilder sb = new StringBuilder();

        sb.append("""

            ═══════════════════════════════════════════
                    ITEMS DUE IN NEXT 7 DAYS
            ═══════════════════════════════════════════

            """);

        Loan[] dueSoon = loanService.getLoansDueSoon(7);

        if (dueSoon.length == 0) {
            sb.append("No items due in the next 7 days.\n");
        } else {
            // Group by due date
            LocalDate currentGroupDate = null;

            for (Loan loan : dueSoon) {
                LocalDate dueDate = loan.getDueDate();

                // Print date header if new date
                if (!dueDate.equals(currentGroupDate)) {
                    currentGroupDate = dueDate;
                    long daysUntil = loan.getDaysUntilDue();
                    String dayLabel = (daysUntil == 0) ? "TODAY"
                        : (daysUntil == 1) ? "TOMORROW"
                        : "In " + daysUntil + " days";

                    sb.append(String.format("%n%s - %s%n",
                        dueDate.format(DateTimeFormatter.ofPattern("EEE, MMM d")),  // (#5:Day name format)
                        dayLabel));
                    sb.append("─".repeat(35)).append("\n");
                }

                sb.append(String.format("  %s → %s%n",
                    loan.getItem().getTitle(),
                    loan.getMember().getName()));
            }
        }

        return sb.toString();
    }

    // Helper to truncate strings for table display
    private String truncate(String text, int maxLength) {
        if (text.length() <= maxLength) return text;
        return text.substring(0, maxLength - 3) + "...";
    }
}
Code Annotations:
  1. Text block: Multi-line string with preserved formatting
  2. formatted(): Like String.format() but called on text block
  3. truncate(): Prevent long titles from breaking table layout
  4. Text block formatting: Triple quotes preserve exact layout
  5. Day name: EEEE pattern gives full day name (Monday, etc.)

Part 5: Complete Example

Step 5.1: Main Application with Reports

package com.library.app;

import com.library.model.*;
import com.library.service.*;
import java.time.LocalDate;

public class LibraryApp {
    public static void main(String[] args) {
        // Initialize services
        MediaCatalog catalog = new MediaCatalog();
        LoanService loanService = new LoanService();
        ReceiptGenerator receiptGen = new ReceiptGenerator();
        ReportGenerator reportGen = new ReportGenerator(loanService);

        // Add sample items
        Book book1 = new Book("B001", "Clean Code", "Robert Martin",
            2008, "978-0132350884", BookCategory.TECHNOLOGY, 464);
        Book book2 = new Book("B002", "The Pragmatic Programmer",
            "David Thomas", 2019, "978-0135957059", BookCategory.TECHNOLOGY, 352);
        DVD dvd1 = new DVD("D001", "Inception", "Christopher Nolan",
            2010, 148, "PG-13");

        catalog.addItem(book1);
        catalog.addItem(book2);
        catalog.addItem(dvd1);

        // Create members
        Member alice = new Member("M001", "Alice Johnson", "alice@email.com");
        Member bob = new Member("M002", "Bob Smith", "bob@email.com");

        // Checkout items
        Loan loan1 = loanService.checkoutItem(book1, alice);
        Loan loan2 = loanService.checkoutItem(dvd1, bob);

        // Print checkout receipt
        System.out.println(receiptGen.generateCheckoutReceipt(loan1));

        // Generate daily report
        System.out.println(reportGen.generateDailySummary());

        // Simulate return (normally would happen days later)
        Loan returnedLoan = loanService.returnItem(loan1.getLoanId());

        // Print return receipt
        System.out.println(receiptGen.generateReturnReceipt(returnedLoan));

        // Generate overdue report
        System.out.println(reportGen.generateOverdueReport());
    }
}

Expected Output (Checkout Receipt)

     =============================================
              City Central Library
               123 Main Street
     =============================================

CHECKOUT RECEIPT
---------------------------------------------
Date: 15 Jan 2025
Time: 14:30:45
Loan ID: L00001

MEMBER
  Name: Alice Johnson
  ID: M001

ITEM BORROWED
  Type: BOOK
  Title: Clean Code
  ID: B001

---------------------------------------------
DUE DATE: 05/02/2025
Loan period: 21 days
Late fee: $0.50 per day
---------------------------------------------

       Thank you for using our library!
         Please return items on time.
     =============================================

Expected Output (Daily Summary)

╔══════════════════════════════════════════════════════════════╗
║                    DAILY LIBRARY REPORT                      ║
╠══════════════════════════════════════════════════════════════╣
║  Date: Wednesday, January 15, 2025                           ║
╠══════════════════════════════════════════════════════════════╣
║  LOAN STATISTICS                                             ║
║  ─────────────────                                           ║
║  Active Loans:          2                                    ║
║  Overdue Items:         0                                    ║
║  Due Within 3 Days:     0                                    ║
║  Outstanding Fees:    $   0.00                               ║
╚══════════════════════════════════════════════════════════════╝

Exercises

Exercise 1: Renewal System

Add loan renewal functionality to LoanService:

  • Add renewLoan(String loanId) method
  • Extend due date by half the original loan period
  • Maximum 2 renewals per loan
  • Cannot renew if item is overdue
  • Generate a renewal receipt

Exercise 2: Reminder Notifications

Create a NotificationService class:

  • Method: generateDueSoonReminder(Loan loan)
  • Use text blocks for email-style message
  • Include: member name, item title, due date, days remaining
  • Different urgency levels: 3 days, 1 day, due today

Exercise 3: Monthly Statistics Report

Add monthly reporting capability:

  • Total loans for the month
  • Total returns (on time vs late)
  • Total late fees collected
  • Most borrowed items (by type)
  • Format as professional report with text blocks

Exercise 4: Date Validation Utility

Create a DateValidator utility class:

  • Method: isValidBookingDate(LocalDate date) - not in past, not > 30 days future
  • Method: isLibraryOpenOn(LocalDate date) - closed on Sundays
  • Method: getNextOpenDay(LocalDate from) - skip closed days
  • Method: formatForDisplay(LocalDate date, String locale)

Deliverables

Bonus Challenges

Advanced Holiday Calendar: Create a holiday calendar that adjusts due dates to skip holidays and weekends
Advanced Receipt PDF: Research how to generate a PDF receipt using a library like iText or Apache PDFBox
Expert Timezone Support: Add support for different library branches in different timezones using ZonedDateTime

Resources