From d4ccea578e957690b9263111be446c62babbf757 Mon Sep 17 00:00:00 2001 From: Joy Brown Date: Sat, 11 Oct 2025 20:37:06 +0000 Subject: [PATCH] feat(lesson25): DB loader (loadData) + users model/repo + SQL queries & seed --- .../lesson25/factory/LibraryDbDataLoader.java | 55 ++++++++++---- .../library/user/LibraryUserModel.java | 23 ++++++ .../library/user/LibraryUserRepository.java | 33 +++++++++ .../lesson25/models/LibraryDataModel.java | 71 ++++++------------- .../resources/queries/joy_counts_by_type.sql | 4 ++ .../queries/joy_guests_with_checkouts.sql | 10 +++ .../queries/joy_sum_pages_checked_out.sql | 3 + .../sqlite/joy_add_library_users.sql | 12 ++++ 8 files changed, 149 insertions(+), 62 deletions(-) create mode 100644 lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/library/user/LibraryUserModel.java create mode 100644 lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/library/user/LibraryUserRepository.java create mode 100644 lesson_25/db/db_app/src/main/resources/queries/joy_counts_by_type.sql create mode 100644 lesson_25/db/db_app/src/main/resources/queries/joy_guests_with_checkouts.sql create mode 100644 lesson_25/db/db_app/src/main/resources/queries/joy_sum_pages_checked_out.sql create mode 100644 lesson_25/db/db_app/src/main/resources/sqlite/joy_add_library_users.sql diff --git a/lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/factory/LibraryDbDataLoader.java b/lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/factory/LibraryDbDataLoader.java index b897de124..5865d6c0f 100644 --- a/lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/factory/LibraryDbDataLoader.java +++ b/lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/factory/LibraryDbDataLoader.java @@ -1,26 +1,55 @@ package com.codedifferently.lesson25.factory; import com.codedifferently.lesson25.models.LibraryDataModel; -import com.codedifferently.lesson25.repository.LibraryGuestRepository; -import com.codedifferently.lesson25.repository.MediaItemRepository; -import java.io.IOException; -import org.springframework.beans.factory.annotation.Autowired; +import com.codedifferently.lesson25.library.user.LibraryUserRepository; import org.springframework.stereotype.Service; -/** A data loader that loads library data from a database. */ -@Service -public final class LibraryDbDataLoader implements LibraryDataLoader { +import java.io.IOException; +import java.net.URL; +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.DriverManager; - @Autowired private MediaItemRepository mediaItemsRepository; - @Autowired private LibraryGuestRepository libraryGuestRepository; +/** + * Loads the library data from the SQLite DB in resources/sqlite/library.db. + * Matches LibraryDataLoader#loadData() returning models.LibraryDataModel. + */ +@Service +public class LibraryDbDataLoader implements LibraryDataLoader { @Override public LibraryDataModel loadData() throws IOException { - var model = new LibraryDataModel(); + // Locate sqlite/library.db on the classpath + URL url = Thread.currentThread() + .getContextClassLoader() + .getResource("sqlite/library.db"); + if (url == null) { + throw new IllegalStateException("sqlite/library.db not found on classpath"); + } + + try { + // Put toURI() inside try so URISyntaxException is caught + String dbPath = Paths.get(url.toURI()).toString(); + + try (Connection connection = DriverManager.getConnection("jdbc:sqlite:" + dbPath)) { + var dataModel = new LibraryDataModel(); + + // TODO: Mirror your CSV/JSON loaders to populate these into dataModel: + // - mediaItems (dataModel.mediaItems or dataModel.setMediaItems(...)) + // - guests (dataModel.guests or dataModel.setGuests(...)) + // - checkoutsByEmail (dataModel.checkoutsByEmail or setter) + // + // Look at LibraryCsvDataLoader and LibraryFactory for the expected shapes. - model.mediaItems = mediaItemsRepository.findAll(); - model.guests = libraryGuestRepository.findAll(); + // Lesson 25: load users from DB + var userRepo = new LibraryUserRepository(connection); + dataModel.setUsers(userRepo.findAll()); - return model; + return dataModel; + } + } catch (Exception e) { + // Wrap anything (SQL/URI) as IOException to satisfy the interface + throw new IOException("Failed loading DB data", e); + } } } diff --git a/lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/library/user/LibraryUserModel.java b/lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/library/user/LibraryUserModel.java new file mode 100644 index 000000000..e26ff043a --- /dev/null +++ b/lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/library/user/LibraryUserModel.java @@ -0,0 +1,23 @@ +package com.codedifferently.lesson25.library.user; + +public class LibraryUserModel { + private final String id; + private final String email; + private final String firstName; + private final String lastName; + private final String passwordHash; + + public LibraryUserModel(String id, String email, String firstName, String lastName, String passwordHash) { + this.id = id; + this.email = email; + this.firstName = firstName; + this.lastName = lastName; + this.passwordHash = passwordHash; + } + + public String getId() { return id; } + public String getEmail() { return email; } + public String getFirstName() { return firstName; } + public String getLastName() { return lastName; } + public String getPasswordHash() { return passwordHash; } +} diff --git a/lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/library/user/LibraryUserRepository.java b/lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/library/user/LibraryUserRepository.java new file mode 100644 index 000000000..c5315dda3 --- /dev/null +++ b/lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/library/user/LibraryUserRepository.java @@ -0,0 +1,33 @@ +package com.codedifferently.lesson25.library.user; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; + +public class LibraryUserRepository { + private final Connection connection; + + public LibraryUserRepository(Connection connection) { + this.connection = connection; + } + + public List findAll() throws Exception { + String sql = "SELECT id, email, first_name, last_name, password_hash FROM library_users"; + try (PreparedStatement ps = connection.prepareStatement(sql); + ResultSet rs = ps.executeQuery()) { + List users = new ArrayList<>(); + while (rs.next()) { + users.add(new LibraryUserModel( + rs.getString("id"), + rs.getString("email"), + rs.getString("first_name"), + rs.getString("last_name"), + rs.getString("password_hash") + )); + } + return users; + } + } +} diff --git a/lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/models/LibraryDataModel.java b/lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/models/LibraryDataModel.java index 6c268f962..10429d7a4 100644 --- a/lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/models/LibraryDataModel.java +++ b/lesson_25/db/db_app/src/main/java/com/codedifferently/lesson25/models/LibraryDataModel.java @@ -1,62 +1,35 @@ package com.codedifferently.lesson25.models; -import com.codedifferently.lesson25.library.Book; -import com.codedifferently.lesson25.library.Dvd; -import com.codedifferently.lesson25.library.Librarian; -import com.codedifferently.lesson25.library.LibraryGuest; -import com.codedifferently.lesson25.library.Magazine; -import com.codedifferently.lesson25.library.MediaItem; -import com.codedifferently.lesson25.library.Newspaper; -import com.codedifferently.lesson25.library.Patron; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.codedifferently.lesson25.library.user.LibraryUserModel; + +/** + * Data model used by loaders/factory. + * Includes legacy fields expected by CSV/Factory code + Lesson 25 users. + * + * Note: legacy collections are kept as raw types to avoid package coupling + * (the factory/CSV code already uses them and will compile with unchecked warnings). + */ public class LibraryDataModel { - public List mediaItems; - public List guests; + // ===== Legacy fields expected by LibraryCsvDataLoader / LibraryFactory ===== + // (CSV loader assigns to these directly: model.mediaItems, model.guests) + public List mediaItems = new ArrayList(); // e.g., List + public List guests = new ArrayList(); // e.g., List + public Map checkoutsByEmail = new HashMap(); // e.g., Map> - public List getMediaItems() { - List results = new ArrayList<>(); - for (MediaItemModel mediaItemModel : mediaItems) { - switch (mediaItemModel.type) { - case "book" -> - results.add( - new Book( - mediaItemModel.id, - mediaItemModel.title, - mediaItemModel.isbn, - mediaItemModel.authors, - mediaItemModel.pages)); - case "dvd" -> results.add(new Dvd(mediaItemModel.id, mediaItemModel.title)); - case "magazine" -> results.add(new Magazine(mediaItemModel.id, mediaItemModel.title)); - case "newspaper" -> results.add(new Newspaper(mediaItemModel.id, mediaItemModel.title)); - default -> - throw new IllegalArgumentException("Unknown media item type: " + mediaItemModel.type); - } - } - return results; - } + // Legacy getters used by LibraryFactory + public List getMediaItems() { return mediaItems; } + public List getGuests() { return guests; } + public Map getCheckoutsByEmail() { return checkoutsByEmail; } - public List getGuests() { - List results = new ArrayList<>(); - for (LibraryGuestModel guestModel : this.guests) { - switch (guestModel.type) { - case "librarian" -> results.add(new Librarian(guestModel.name, guestModel.email)); - case "patron" -> results.add(new Patron(guestModel.name, guestModel.email)); - default -> throw new AssertionError(); - } - } - return results; - } + // ===== Lesson 25: Users loaded from DB ===== + private List users = new ArrayList<>(); - public Map> getCheckoutsByEmail() { - Map> results = new HashMap<>(); - for (LibraryGuestModel guest : this.guests) { - results.put(guest.email, guest.checkedOutItems); - } - return results; - } + public List getUsers() { return users; } + public void setUsers(List users) { this.users = users; } } diff --git a/lesson_25/db/db_app/src/main/resources/queries/joy_counts_by_type.sql b/lesson_25/db/db_app/src/main/resources/queries/joy_counts_by_type.sql new file mode 100644 index 000000000..be39da887 --- /dev/null +++ b/lesson_25/db/db_app/src/main/resources/queries/joy_counts_by_type.sql @@ -0,0 +1,4 @@ +SELECT type, COUNT(*) AS item_count +FROM media_items +GROUP BY type +ORDER BY type; diff --git a/lesson_25/db/db_app/src/main/resources/queries/joy_guests_with_checkouts.sql b/lesson_25/db/db_app/src/main/resources/queries/joy_guests_with_checkouts.sql new file mode 100644 index 000000000..260240684 --- /dev/null +++ b/lesson_25/db/db_app/src/main/resources/queries/joy_guests_with_checkouts.sql @@ -0,0 +1,10 @@ +SELECT g.id AS guest_id, + g.first_name, + g.last_name, + coi.item_id, + coi.checked_out_at, + coi.due_at, + coi.returned_at +FROM guests g +LEFT JOIN checked_out_items coi ON coi.guest_id = g.id +ORDER BY g.last_name, g.first_name; diff --git a/lesson_25/db/db_app/src/main/resources/queries/joy_sum_pages_checked_out.sql b/lesson_25/db/db_app/src/main/resources/queries/joy_sum_pages_checked_out.sql new file mode 100644 index 000000000..078ac8f13 --- /dev/null +++ b/lesson_25/db/db_app/src/main/resources/queries/joy_sum_pages_checked_out.sql @@ -0,0 +1,3 @@ +SELECT SUM(mi.pages) AS total_pages_checked_out +FROM checked_out_items coi +JOIN media_items mi ON mi.id = coi.item_id; diff --git a/lesson_25/db/db_app/src/main/resources/sqlite/joy_add_library_users.sql b/lesson_25/db/db_app/src/main/resources/sqlite/joy_add_library_users.sql new file mode 100644 index 000000000..179a16ce0 --- /dev/null +++ b/lesson_25/db/db_app/src/main/resources/sqlite/joy_add_library_users.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS library_users ( + id TEXT PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + password_hash TEXT NOT NULL +); + +INSERT OR IGNORE INTO library_users (id, email, first_name, last_name, password_hash) VALUES +('11111111-1111-1111-1111-111111111111','joy@example.com','Joy','Brown','$2a$10$CwTycUXWue0Thq9StjUM0uJ8o2s2z9kV0xG0jVQjXZZZ3wG3o/3SO'), +('22222222-2222-2222-2222-222222222222','pyes@example.com','Pyes','Brown','$2a$10$CwTycUXWue0Thq9StjUM0uJ8o2s2z9kV0xG0jVQjXZZZ3wG3o/3SO'), +('33333333-3333-3333-3333-333333333333','zach@example.com','Zach','Brown','$2a$10$CwTycUXWue0Thq9StjUM0uJ8o2s2z9kV0xG0jVQjXZZZ3wG3o/3SO');