Library Management System - Loan Management
Implement loan tracking with dates, receipts, and reports using modern Java APIs
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:
- LocalDateTime: Captures exact moment (date + time)
- DateTimeFormatter: Custom format pattern for display
- now(): Get current date-time
- plusDays(): Add days to calculate due date
- isAfter(): Compare two dates
- ChronoUnit.DAYS.between(): Calculate exact days between dates
- 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:
- Static utility: Parse date without instance, handles exceptions
- 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:
- StringBuilder: Efficiently builds string without creating many intermediate objects
- Formatted dates: User-friendly date format for receipts
- Period: Duration of the loan in date terms
- Currency format:
%.2fshows 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:
- Text block: Multi-line string with preserved formatting
- formatted(): Like String.format() but called on text block
- truncate(): Prevent long titles from breaking table layout
- Text block formatting: Triple quotes preserve exact layout
- 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