Library Management System - Complete Implementation
Refactor to use Collections, add file persistence, and implement proper exception handling
Objectives
By the end of this practical work, you will be able to:
- Replace arrays with ArrayList for dynamic collections
- Use HashMap for efficient lookups by ID
- Implement Optional for null-safe returns
- Save and load library data using CSV files
- Handle exceptions properly with try-catch-finally
- Use try-with-resources for automatic resource management
Prerequisites
- Completed Practical Works 3-7
- MediaItem hierarchy (Book, DVD, Magazine)
- Loan and LoanService classes
- Member class
Project Structure
Final project structure with all components:
library-system
src
com.library
model
MediaItem.java
Book.java
DVD.java
Magazine.java
Member.java
Loan.java
service
MediaCatalog.java
MemberService.java
LoanService.java
persistence
CsvMediaRepository.java
CsvMemberRepository.java
CsvLoanRepository.java
exception
LibraryException.java
ItemNotFoundException.java
ItemNotAvailableException.java
app
LibraryApp.java
data
media.csv
members.csv
loans.csv
Part 1: Custom Exceptions
Step 1.1: Create Exception Hierarchy
Create custom exceptions for the library domain:
package com.library.exception;
// Base exception for all library errors
public class LibraryException extends Exception { // (#1:Checked exception)
public LibraryException(String message) {
super(message);
}
public LibraryException(String message, Throwable cause) { // (#2:Chain exceptions)
super(message, cause);
}
}
package com.library.exception;
public class ItemNotFoundException extends LibraryException { // (#3:Specific exception)
private final String itemId;
public ItemNotFoundException(String itemId) {
super("Item not found: " + itemId);
this.itemId = itemId;
}
public String getItemId() { return itemId; }
}
package com.library.exception;
public class ItemNotAvailableException extends LibraryException {
private final String itemId;
private final String reason;
public ItemNotAvailableException(String itemId, String reason) {
super("Item " + itemId + " is not available: " + reason);
this.itemId = itemId;
this.reason = reason;
}
public String getItemId() { return itemId; }
public String getReason() { return reason; }
}
Code Annotations:
- Checked exception: Extends Exception - must be declared or caught
- Exception chaining: Preserves original cause for debugging
- Specific exception: More meaningful than generic Exception
Part 2: Refactored MediaCatalog with Collections
Step 2.1: Replace Arrays with ArrayList and HashMap
package com.library.service;
import com.library.model.*;
import com.library.exception.*;
import java.util.*;
public class MediaCatalog {
private final List<MediaItem> items; // (#1:ArrayList for ordered collection)
private final Map<String, MediaItem> itemsById; // (#2:HashMap for O(1) lookup)
public MediaCatalog() {
this.items = new ArrayList<>();
this.itemsById = new HashMap<>();
}
// Add item to catalog
public void addItem(MediaItem item) {
if (itemsById.containsKey(item.getId())) {
throw new IllegalArgumentException(
"Item with ID " + item.getId() + " already exists");
}
items.add(item);
itemsById.put(item.getId(), item);
}
// Find by ID using Optional (#3:Null-safe return)
public Optional<MediaItem> findById(String id) {
return Optional.ofNullable(itemsById.get(id));
}
// Get by ID or throw exception
public MediaItem getById(String id) throws ItemNotFoundException {
return findById(id)
.orElseThrow(() -> new ItemNotFoundException(id)); // (#4:Optional with exception)
}
// Remove item from catalog
public boolean removeItem(String id) {
MediaItem item = itemsById.remove(id);
if (item != null) {
items.remove(item);
return true;
}
return false;
}
// Search by title (case-insensitive)
public List<MediaItem> searchByTitle(String searchTerm) {
List<MediaItem> results = new ArrayList<>();
String lowerSearch = searchTerm.toLowerCase();
for (MediaItem item : items) {
if (item.getTitle().toLowerCase().contains(lowerSearch)) {
results.add(item);
}
}
return results;
}
// Filter by status
public List<MediaItem> getByStatus(MediaStatus status) {
List<MediaItem> results = new ArrayList<>();
for (MediaItem item : items) {
if (item.getStatus() == status) {
results.add(item);
}
}
return results;
}
// Get all books using instanceof
public List<Book> getAllBooks() {
List<Book> books = new ArrayList<>();
for (MediaItem item : items) {
if (item instanceof Book book) { // (#5:Pattern matching)
books.add(book);
}
}
return books;
}
// Get all DVDs
public List<DVD> getAllDVDs() {
List<DVD> dvds = new ArrayList<>();
for (MediaItem item : items) {
if (item instanceof DVD dvd) {
dvds.add(dvd);
}
}
return dvds;
}
// Statistics
public Map<String, Integer> getStatsByType() { // (#6:Map for statistics)
Map<String, Integer> stats = new HashMap<>();
stats.put("BOOK", 0);
stats.put("DVD", 0);
stats.put("MAGAZINE", 0);
for (MediaItem item : items) {
String type = item.getMediaType();
stats.put(type, stats.get(type) + 1);
}
return stats;
}
// Getters
public List<MediaItem> getAllItems() {
return new ArrayList<>(items); // (#7:Return copy to protect internal state)
}
public int size() {
return items.size();
}
public boolean isEmpty() {
return items.isEmpty();
}
}
Code Annotations:
- ArrayList: Dynamic size, maintains insertion order
- HashMap: O(1) lookups by key (ID)
- Optional: Explicitly signals value might be absent
- orElseThrow: Convert Optional to exception if empty
- Pattern matching: Safe type check and cast
- Map for stats: Store counts by type
- Defensive copy: Prevent external modification
Part 3: Member Service with HashSet
Step 3.1: Create MemberService Class
package com.library.service;
import com.library.model.*;
import com.library.exception.*;
import java.util.*;
public class MemberService {
private final Map<String, Member> membersById;
private final Set<String> emails; // (#1:HashSet for unique emails)
public MemberService() {
this.membersById = new HashMap<>();
this.emails = new HashSet<>();
}
// Register new member
public void registerMember(Member member) throws LibraryException {
// Check for duplicate ID
if (membersById.containsKey(member.getId())) {
throw new LibraryException(
"Member with ID " + member.getId() + " already exists");
}
// Check for duplicate email (#2:Set contains check is O(1))
if (emails.contains(member.getEmail().toLowerCase())) {
throw new LibraryException(
"Email " + member.getEmail() + " is already registered");
}
membersById.put(member.getId(), member);
emails.add(member.getEmail().toLowerCase());
}
// Find by ID
public Optional<Member> findById(String id) {
return Optional.ofNullable(membersById.get(id));
}
// Get or throw
public Member getById(String id) throws ItemNotFoundException {
return findById(id)
.orElseThrow(() -> new ItemNotFoundException(id));
}
// Find by email
public Optional<Member> findByEmail(String email) {
String lowerEmail = email.toLowerCase();
for (Member member : membersById.values()) { // (#3:Iterate over map values)
if (member.getEmail().toLowerCase().equals(lowerEmail)) {
return Optional.of(member);
}
}
return Optional.empty();
}
// Update member
public void updateMember(Member member) throws ItemNotFoundException {
if (!membersById.containsKey(member.getId())) {
throw new ItemNotFoundException(member.getId());
}
Member existing = membersById.get(member.getId());
// Update email set if changed
if (!existing.getEmail().equalsIgnoreCase(member.getEmail())) {
emails.remove(existing.getEmail().toLowerCase());
emails.add(member.getEmail().toLowerCase());
}
membersById.put(member.getId(), member);
}
// Get all members
public List<Member> getAllMembers() {
return new ArrayList<>(membersById.values());
}
// Get member count
public int getMemberCount() {
return membersById.size();
}
}
Code Annotations:
- HashSet: Guarantees unique values, O(1) contains check
- Set contains: Fast duplicate detection
- Map.values(): Get all values from map
Part 4: Loan Service with Collections
Step 4.1: Refactor LoanService
package com.library.service;
import com.library.model.*;
import com.library.exception.*;
import java.util.*;
import java.time.LocalDate;
public class LoanService {
private final List<Loan> activeLoans;
private final List<Loan> loanHistory;
private final Map<String, Loan> loansById;
private final Map<String, List<Loan>> loansByMember; // (#1:Map with List values)
private int loanIdCounter;
public LoanService() {
this.activeLoans = new ArrayList<>();
this.loanHistory = new ArrayList<>();
this.loansById = new HashMap<>();
this.loansByMember = new HashMap<>();
this.loanIdCounter = 1;
}
// Create a new loan
public Loan checkoutItem(MediaItem item, Member member)
throws ItemNotAvailableException {
if (!item.getStatus().canBeBorrowed()) {
throw new ItemNotAvailableException(
item.getId(),
item.getStatus().getDescription());
}
String loanId = generateLoanId();
Loan loan = new Loan(loanId, item, member, item.getMaxLoanDays());
item.setStatus(MediaStatus.BORROWED);
activeLoans.add(loan);
loansById.put(loanId, loan);
// Add to member's loan list (#2:Compute if absent pattern)
loansByMember.computeIfAbsent(member.getId(), k -> new ArrayList<>())
.add(loan);
return loan;
}
// Return an item
public Loan returnItem(String loanId) throws ItemNotFoundException {
Loan loan = loansById.get(loanId);
if (loan == null || loan.isReturned()) {
throw new ItemNotFoundException(loanId);
}
loan.returnItem();
loan.getItem().setStatus(MediaStatus.AVAILABLE);
activeLoans.remove(loan);
loanHistory.add(loan);
return loan;
}
// Get loans for a member
public List<Loan> getLoansByMember(String memberId) {
return loansByMember.getOrDefault(memberId, Collections.emptyList()); // (#3:Safe default)
}
// Get active loans for a member
public List<Loan> getActiveLoansForMember(String memberId) {
List<Loan> result = new ArrayList<>();
for (Loan loan : getLoansByMember(memberId)) {
if (!loan.isReturned()) {
result.add(loan);
}
}
return result;
}
// Get overdue loans
public List<Loan> getOverdueLoans() {
List<Loan> overdue = new ArrayList<>();
for (Loan loan : activeLoans) {
if (loan.isOverdue()) {
overdue.add(loan);
}
}
return overdue;
}
// Get loans due within days
public List<Loan> getLoansDueSoon(int withinDays) {
List<Loan> dueSoon = new ArrayList<>();
LocalDate threshold = LocalDate.now().plusDays(withinDays);
for (Loan loan : activeLoans) {
if (!loan.isOverdue() &&
!loan.getDueDate().isAfter(threshold)) {
dueSoon.add(loan);
}
}
return dueSoon;
}
// Find loan by ID
public Optional<Loan> findById(String loanId) {
return Optional.ofNullable(loansById.get(loanId));
}
// Calculate total outstanding fees
public double getTotalOutstandingFees() {
double total = 0.0;
for (Loan loan : activeLoans) {
if (loan.isOverdue()) {
total += loan.getDaysOverdue() * loan.getItem().getLateFeePerDay();
}
}
return total;
}
private String generateLoanId() {
return String.format("L%05d", loanIdCounter++);
}
// For persistence
public void setLoanIdCounter(int counter) {
this.loanIdCounter = counter;
}
public List<Loan> getActiveLoans() { return new ArrayList<>(activeLoans); }
public List<Loan> getLoanHistory() { return new ArrayList<>(loanHistory); }
public int getActiveCount() { return activeLoans.size(); }
}
Code Annotations:
- Map<String, List>: One-to-many relationship
- computeIfAbsent: Create list only when needed
- getOrDefault: Return empty list instead of null
Part 5: CSV File Persistence
Step 5.1: Create CsvMediaRepository
package com.library.persistence;
import com.library.model.*;
import java.io.*;
import java.util.*;
public class CsvMediaRepository {
private static final String DELIMITER = ",";
private static final String HEADER = "type,id,title,year,status,extraFields";
// Save all media items to CSV
public void saveAll(List<MediaItem> items, String filename)
throws IOException {
try (PrintWriter writer = new PrintWriter( // (#1:Try-with-resources)
new BufferedWriter(new FileWriter(filename)))) {
writer.println(HEADER);
for (MediaItem item : items) {
writer.println(formatItem(item));
}
} // (#2:Automatically closes writer)
}
// Format item as CSV line
private String formatItem(MediaItem item) {
StringBuilder sb = new StringBuilder();
// Common fields
sb.append(item.getMediaType()).append(DELIMITER);
sb.append(escapeField(item.getId())).append(DELIMITER);
sb.append(escapeField(item.getTitle())).append(DELIMITER);
sb.append(item.getYear()).append(DELIMITER);
sb.append(item.getStatus().name()).append(DELIMITER);
// Type-specific fields
if (item instanceof Book book) {
sb.append(escapeField(book.getAuthor())).append(DELIMITER);
sb.append(escapeField(book.getIsbn())).append(DELIMITER);
sb.append(book.getCategory().name()).append(DELIMITER);
sb.append(book.getPageCount());
} else if (item instanceof DVD dvd) {
sb.append(escapeField(dvd.getDirector())).append(DELIMITER);
sb.append(dvd.getDurationMinutes()).append(DELIMITER);
sb.append(escapeField(dvd.getRating()));
} else if (item instanceof Magazine mag) {
sb.append(escapeField(mag.getPublisher())).append(DELIMITER);
sb.append(mag.getIssueNumber()).append(DELIMITER);
sb.append(escapeField(mag.getMonth()));
}
return sb.toString();
}
// Load all media items from CSV
public List<MediaItem> loadAll(String filename) throws IOException {
List<MediaItem> items = new ArrayList<>();
File file = new File(filename);
if (!file.exists()) { // (#3:Handle missing file)
return items; // Return empty list
}
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line;
boolean isFirstLine = true;
while ((line = reader.readLine()) != null) {
if (isFirstLine) { // (#4:Skip header)
isFirstLine = false;
continue;
}
try {
MediaItem item = parseLine(line);
if (item != null) {
items.add(item);
}
} catch (Exception e) { // (#5:Handle malformed lines)
System.err.println("Warning: Could not parse line: " + line);
}
}
}
return items;
}
// Parse a CSV line into MediaItem
private MediaItem parseLine(String line) {
String[] parts = splitCsvLine(line);
String type = parts[0];
String id = unescapeField(parts[1]);
String title = unescapeField(parts[2]);
int year = Integer.parseInt(parts[3]);
MediaStatus status = MediaStatus.valueOf(parts[4]);
MediaItem item;
switch (type) {
case "BOOK" -> {
String author = unescapeField(parts[5]);
String isbn = unescapeField(parts[6]);
BookCategory category = BookCategory.valueOf(parts[7]);
int pages = Integer.parseInt(parts[8]);
item = new Book(id, title, author, year, isbn, category, pages);
}
case "DVD" -> {
String director = unescapeField(parts[5]);
int duration = Integer.parseInt(parts[6]);
String rating = unescapeField(parts[7]);
item = new DVD(id, title, director, year, duration, rating);
}
case "MAGAZINE" -> {
String publisher = unescapeField(parts[5]);
int issue = Integer.parseInt(parts[6]);
String month = unescapeField(parts[7]);
item = new Magazine(id, title, publisher, year, issue, month);
}
default -> {
return null;
}
}
item.setStatus(status);
return item;
}
// Simple CSV line splitter (handles quoted fields)
private String[] splitCsvLine(String line) {
List<String> parts = new ArrayList<>();
StringBuilder current = new StringBuilder();
boolean inQuotes = false;
for (char c : line.toCharArray()) {
if (c == '"') {
inQuotes = !inQuotes;
} else if (c == ',' && !inQuotes) {
parts.add(current.toString());
current = new StringBuilder();
} else {
current.append(c);
}
}
parts.add(current.toString());
return parts.toArray(new String[0]);
}
// Escape field with quotes if needed
private String escapeField(String field) {
if (field.contains(",") || field.contains("\"")) {
return "\"" + field.replace("\"", "\"\"") + "\"";
}
return field;
}
private String unescapeField(String field) {
if (field.startsWith("\"") && field.endsWith("\"")) {
return field.substring(1, field.length() - 1)
.replace("\"\"", "\"");
}
return field;
}
}
Code Annotations:
- Try-with-resources: Guarantees resource closure
- Auto-close: No need for finally block
- File exists check: Graceful handling of first run
- Skip header: CSV files typically have headers
- Error handling: Don't fail on single bad line
Step 5.2: Create CsvMemberRepository
package com.library.persistence;
import com.library.model.*;
import java.io.*;
import java.util.*;
public class CsvMemberRepository {
private static final String HEADER = "id,name,email,phone,joinDate";
public void saveAll(List<Member> members, String filename)
throws IOException {
try (PrintWriter writer = new PrintWriter(
new BufferedWriter(new FileWriter(filename)))) {
writer.println(HEADER);
for (Member member : members) {
writer.printf("%s,%s,%s,%s,%s%n",
member.getId(),
escapeField(member.getName()),
member.getEmail(),
member.getPhone() != null ? member.getPhone() : "",
member.getJoinDate());
}
}
}
public List<Member> loadAll(String filename) throws IOException {
List<Member> members = new ArrayList<>();
File file = new File(filename);
if (!file.exists()) {
return members;
}
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line;
boolean isFirstLine = true;
while ((line = reader.readLine()) != null) {
if (isFirstLine) {
isFirstLine = false;
continue;
}
try {
String[] parts = line.split(",", -1); // (#1:Keep empty parts)
Member member = new Member(
parts[0],
unescapeField(parts[1]),
parts[2]
);
if (!parts[3].isEmpty()) {
member.setPhone(parts[3]);
}
members.add(member);
} catch (Exception e) {
System.err.println("Warning: Could not parse member: " + line);
}
}
}
return members;
}
private String escapeField(String field) {
if (field.contains(",")) {
return "\"" + field + "\"";
}
return field;
}
private String unescapeField(String field) {
if (field.startsWith("\"") && field.endsWith("\"")) {
return field.substring(1, field.length() - 1);
}
return field;
}
}
Code Annotations:
- split with -1: Preserves trailing empty strings
Part 6: Complete Library Application
Step 6.1: Main Application with Persistence
package com.library.app;
import com.library.model.*;
import com.library.service.*;
import com.library.persistence.*;
import com.library.exception.*;
import java.io.*;
import java.util.*;
public class LibraryApp {
private static final String DATA_DIR = "data/";
private static final String MEDIA_FILE = DATA_DIR + "media.csv";
private static final String MEMBERS_FILE = DATA_DIR + "members.csv";
private final MediaCatalog catalog;
private final MemberService memberService;
private final LoanService loanService;
private final CsvMediaRepository mediaRepo;
private final CsvMemberRepository memberRepo;
private final Scanner scanner;
public LibraryApp() {
this.catalog = new MediaCatalog();
this.memberService = new MemberService();
this.loanService = new LoanService();
this.mediaRepo = new CsvMediaRepository();
this.memberRepo = new CsvMemberRepository();
this.scanner = new Scanner(System.in);
}
public void start() {
System.out.println("=== Library Management System ===\n");
// Load data on startup
loadData();
boolean running = true;
while (running) {
displayMenu();
String choice = scanner.nextLine().trim();
try {
running = processChoice(choice);
} catch (LibraryException e) {
System.out.println("Error: " + e.getMessage());
} catch (Exception e) {
System.out.println("Unexpected error: " + e.getMessage());
}
}
// Save data on exit
saveData();
System.out.println("Goodbye!");
}
private void loadData() {
try {
// Load media items
List<MediaItem> items = mediaRepo.loadAll(MEDIA_FILE);
for (MediaItem item : items) {
catalog.addItem(item);
}
System.out.printf("Loaded %d media items%n", items.size());
// Load members
List<Member> members = memberRepo.loadAll(MEMBERS_FILE);
for (Member member : members) {
memberService.registerMember(member);
}
System.out.printf("Loaded %d members%n", members.size());
} catch (IOException e) {
System.out.println("Note: Could not load data - " + e.getMessage());
} catch (LibraryException e) {
System.out.println("Error loading data: " + e.getMessage());
}
}
private void saveData() {
try {
// Create data directory if needed
new File(DATA_DIR).mkdirs(); // (#1:Ensure directory exists)
mediaRepo.saveAll(catalog.getAllItems(), MEDIA_FILE);
memberRepo.saveAll(memberService.getAllMembers(), MEMBERS_FILE);
System.out.println("Data saved successfully");
} catch (IOException e) {
System.out.println("Error saving data: " + e.getMessage());
}
}
private void displayMenu() {
System.out.println("""
╔══════════════════════════════════════╗
║ LIBRARY MENU ║
╠══════════════════════════════════════╣
║ 1. Browse Catalog ║
║ 2. Search Items ║
║ 3. Add New Item ║
║ 4. Checkout Item ║
║ 5. Return Item ║
║ 6. View Active Loans ║
║ 7. Register Member ║
║ 8. View Statistics ║
║ 0. Exit ║
╚══════════════════════════════════════╝
""");
System.out.print("Enter choice: ");
}
private boolean processChoice(String choice) throws LibraryException {
switch (choice) {
case "1" -> browseCatalog();
case "2" -> searchItems();
case "3" -> addNewItem();
case "4" -> checkoutItem();
case "5" -> returnItem();
case "6" -> viewActiveLoans();
case "7" -> registerMember();
case "8" -> viewStatistics();
case "0" -> { return false; }
default -> System.out.println("Invalid choice");
}
return true;
}
private void browseCatalog() {
List<MediaItem> items = catalog.getAllItems();
if (items.isEmpty()) {
System.out.println("Catalog is empty");
return;
}
System.out.println("\n=== Library Catalog ===");
int index = 1;
for (MediaItem item : items) {
System.out.printf("%3d. [%s] %s%n", index++, item.getMediaType(), item);
}
}
private void searchItems() {
System.out.print("Enter search term: ");
String term = scanner.nextLine();
List<MediaItem> results = catalog.searchByTitle(term);
if (results.isEmpty()) {
System.out.println("No items found matching: " + term);
} else {
System.out.println("\nSearch Results:");
for (MediaItem item : results) {
System.out.println(" - " + item);
}
}
}
private void addNewItem() {
System.out.println("Item type: 1=Book, 2=DVD, 3=Magazine");
String typeChoice = scanner.nextLine();
System.out.print("ID: ");
String id = scanner.nextLine();
System.out.print("Title: ");
String title = scanner.nextLine();
System.out.print("Year: ");
int year = Integer.parseInt(scanner.nextLine());
MediaItem item;
switch (typeChoice) {
case "1" -> {
System.out.print("Author: ");
String author = scanner.nextLine();
System.out.print("ISBN: ");
String isbn = scanner.nextLine();
System.out.print("Category (FICTION/NON_FICTION/TECHNOLOGY/SCIENCE): ");
BookCategory cat = BookCategory.valueOf(scanner.nextLine().toUpperCase());
item = new Book(id, title, author, year, isbn, cat);
}
case "2" -> {
System.out.print("Director: ");
String director = scanner.nextLine();
System.out.print("Duration (minutes): ");
int duration = Integer.parseInt(scanner.nextLine());
System.out.print("Rating: ");
String rating = scanner.nextLine();
item = new DVD(id, title, director, year, duration, rating);
}
case "3" -> {
System.out.print("Publisher: ");
String publisher = scanner.nextLine();
System.out.print("Issue number: ");
int issue = Integer.parseInt(scanner.nextLine());
System.out.print("Month: ");
String month = scanner.nextLine();
item = new Magazine(id, title, publisher, year, issue, month);
}
default -> {
System.out.println("Invalid type");
return;
}
}
catalog.addItem(item);
System.out.println("Item added: " + item);
}
private void checkoutItem() throws LibraryException {
System.out.print("Item ID: ");
String itemId = scanner.nextLine();
System.out.print("Member ID: ");
String memberId = scanner.nextLine();
MediaItem item = catalog.getById(itemId);
Member member = memberService.getById(memberId);
Loan loan = loanService.checkoutItem(item, member);
System.out.println("Checkout successful! Loan ID: " + loan.getLoanId());
System.out.println("Due date: " + loan.getFormattedDueDate());
}
private void returnItem() throws LibraryException {
System.out.print("Loan ID: ");
String loanId = scanner.nextLine();
Loan loan = loanService.returnItem(loanId);
System.out.println("Return successful!");
if (loan.getLateFeeCharged() > 0) {
System.out.printf("Late fee: $%.2f%n", loan.getLateFeeCharged());
}
}
private void viewActiveLoans() {
List<Loan> loans = loanService.getActiveLoans();
if (loans.isEmpty()) {
System.out.println("No active loans");
return;
}
System.out.println("\n=== Active Loans ===");
for (Loan loan : loans) {
String status = loan.isOverdue() ? "[OVERDUE]" : "";
System.out.printf("%s: %s → %s (due: %s) %s%n",
loan.getLoanId(),
loan.getItem().getTitle(),
loan.getMember().getName(),
loan.getFormattedDueDate(),
status);
}
}
private void registerMember() throws LibraryException {
System.out.print("Member ID: ");
String id = scanner.nextLine();
System.out.print("Name: ");
String name = scanner.nextLine();
System.out.print("Email: ");
String email = scanner.nextLine();
Member member = new Member(id, name, email);
memberService.registerMember(member);
System.out.println("Member registered: " + name);
}
private void viewStatistics() {
System.out.println("\n=== Library Statistics ===");
System.out.printf("Total items: %d%n", catalog.size());
Map<String, Integer> stats = catalog.getStatsByType();
for (Map.Entry<String, Integer> entry : stats.entrySet()) {
System.out.printf(" %s: %d%n", entry.getKey(), entry.getValue());
}
System.out.printf("Total members: %d%n", memberService.getMemberCount());
System.out.printf("Active loans: %d%n", loanService.getActiveCount());
System.out.printf("Overdue items: %d%n", loanService.getOverdueLoans().size());
System.out.printf("Outstanding fees: $%.2f%n", loanService.getTotalOutstandingFees());
}
public static void main(String[] args) {
new LibraryApp().start();
}
}
Code Annotations:
- mkdirs(): Creates directory and parents if needed
Exercises
Exercise 1: Add Loan Persistence
Create a CsvLoanRepository class:
- Save active loans with: loanId, itemId, memberId, checkoutDate, dueDate
- Load loans and reconnect to items and members
- Integrate with LibraryApp startup/shutdown
Exercise 2: Transaction History
Track all transactions in a log file:
- Create
TransactionLoggerclass - Log checkouts, returns, registrations with timestamps
- Use append mode (FileWriter with true)
- Add method to read and display recent transactions
Exercise 3: Export Reports
Add report export functionality:
- Export overdue report to
reports/overdue.csv - Export member activity report
- Export monthly statistics
- Use proper exception handling for all file operations
Exercise 4: Import Batch Data
Create a batch import feature:
- Import books from ISBN list file
- Import members from CSV file
- Report success/failure counts
- Handle duplicate IDs gracefully
Deliverables
Bonus Challenges
Advanced
JSON Format: Create JSON repositories using Gson or Jackson library
Advanced
Backup System: Implement automatic backups with timestamps before save
Expert
Configuration File: Add a properties file for settings (data directory, max loans, late fee rates)