← Back to Lecture
Practical Work 8

Library Management System - Complete Implementation

Refactor to use Collections, add file persistence, and implement proper exception handling

Duration 4 hours
Difficulty Intermediate
Session 8 - Collections, File I/O, Exceptions

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:
  1. Checked exception: Extends Exception - must be declared or caught
  2. Exception chaining: Preserves original cause for debugging
  3. 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:
  1. ArrayList: Dynamic size, maintains insertion order
  2. HashMap: O(1) lookups by key (ID)
  3. Optional: Explicitly signals value might be absent
  4. orElseThrow: Convert Optional to exception if empty
  5. Pattern matching: Safe type check and cast
  6. Map for stats: Store counts by type
  7. 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:
  1. HashSet: Guarantees unique values, O(1) contains check
  2. Set contains: Fast duplicate detection
  3. 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:
  1. Map<String, List>: One-to-many relationship
  2. computeIfAbsent: Create list only when needed
  3. 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:
  1. Try-with-resources: Guarantees resource closure
  2. Auto-close: No need for finally block
  3. File exists check: Graceful handling of first run
  4. Skip header: CSV files typically have headers
  5. 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:
  1. 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:
  1. 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 TransactionLogger class
  • 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)

Resources