From e746035551a7f7840f3f9873c57ea9e89f33076f Mon Sep 17 00:00:00 2001
From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com>
Date: Fri, 20 Mar 2026 17:00:03 +0100
Subject: [PATCH 01/13] feat[app]: overwrite close method to export information
to database on exit
---
src/main/java/edu/group5/app/App.java | 52 ++++++++++++++++++++-------
1 file changed, 39 insertions(+), 13 deletions(-)
diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java
index 782922c..5870b77 100644
--- a/src/main/java/edu/group5/app/App.java
+++ b/src/main/java/edu/group5/app/App.java
@@ -3,10 +3,12 @@
import edu.group5.app.control.MainController;
import edu.group5.app.control.wrapper.DbWrapper;
import edu.group5.app.control.wrapper.OrgApiWrapper;
+import edu.group5.app.model.donation.Donation;
import edu.group5.app.model.donation.DonationRepository;
import edu.group5.app.model.donation.DonationService;
import edu.group5.app.model.organization.OrganizationRepository;
import edu.group5.app.model.organization.OrganizationService;
+import edu.group5.app.model.user.User;
import edu.group5.app.model.user.UserRepository;
import edu.group5.app.model.user.UserService;
import javafx.application.Application;
@@ -16,19 +18,30 @@
import java.util.List;
+import java.util.logging.Logger;
+
/**
* Main entry point for the Help-Me-Help charity donation application.
* Handles database connection, data loading, and application setup.
*/
public class App extends Application {
+ DbWrapper dbWrapper;
+ UserRepository userRepository;
+ DonationRepository donationRepository;
+ private Logger logger;
+ private MainController controller;
+ private Scene scene;
+
@Override
- public void start(Stage stage) {
- DbWrapper dbWrapper = new DbWrapper(true);
+ public void init() {
+ this.logger = Logger.getLogger(App.class.getName());
+ this.logger.info("Application starting");
+
+ this.dbWrapper = new DbWrapper(false);
OrgApiWrapper orgApiWrapper = new OrgApiWrapper("https://app.innsamlingskontrollen.no/api/public/v1/all");
- if (!dbWrapper.connect()) {
- System.err.println("Failed to connect to database");
- return;
+ while (!dbWrapper.connect()) {
+ this.logger.warning("Failed to connect to database");
}
// Load data from database
@@ -47,26 +60,39 @@ public void start(Stage stage) {
}
// Create repositories with fetched data
- UserRepository userRepository = new UserRepository(userData);
- DonationRepository donationRepository = new DonationRepository(donationData);
+ this.userRepository = new UserRepository(userData);
+ this.donationRepository = new DonationRepository(donationData);
OrganizationRepository organizationRepository = new OrganizationRepository(organizationData);
// Create services (backend wiring)
- UserService userService = new UserService(userRepository);
- DonationService donationService = new DonationService(donationRepository, organizationRepository);
+ UserService userService = new UserService(this.userRepository);
+ DonationService donationService = new DonationService(this.donationRepository, organizationRepository);
OrganizationService organizationService = new OrganizationService(organizationRepository);
- MainController controller = new MainController(userService, donationService, organizationService);
+ this.controller = new MainController(userService, donationService, organizationService);
- Scene scene = controller.getMainView().getScene();
- controller.showLoginPage();
+ this.scene = controller.getMainView().getScene();
+ }
+
+ @Override
+ public void start(Stage stage) {
+ this.controller.showLoginPage();
stage.getIcons().add(new Image(getClass().getResource("/header/images/hmh-logo.png").toExternalForm()));
stage.setTitle("Help-Me-Help");
- stage.setScene(scene);
+ stage.setScene(this.scene);
stage.show();
}
+ @Override
+ public void stop() {
+ this.logger.info("Application stopping");
+ this.dbWrapper.connect();
+ this.dbWrapper.exportUsers(this.userRepository.export());
+ this.dbWrapper.exportDonations(this.donationRepository.export());
+ this.dbWrapper.disconnect();
+ }
+
public static void main(String[] args) {
launch(args);
}
From ab5ab0614c3ebf6678dbb6a159e2fa9091a9ed7a Mon Sep 17 00:00:00 2001
From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com>
Date: Fri, 20 Mar 2026 17:11:52 +0100
Subject: [PATCH 02/13] fix[app]: properly call the stop super method when
overriding
---
src/main/java/edu/group5/app/App.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java
index 5870b77..d657fb8 100644
--- a/src/main/java/edu/group5/app/App.java
+++ b/src/main/java/edu/group5/app/App.java
@@ -85,7 +85,8 @@ public void start(Stage stage) {
}
@Override
- public void stop() {
+ public void stop() throws Exception {
+ super.stop();
this.logger.info("Application stopping");
this.dbWrapper.connect();
this.dbWrapper.exportUsers(this.userRepository.export());
From 4444322d068a9705ba335f7652e3629a02f44512 Mon Sep 17 00:00:00 2001
From: Lucy Ciara Herud-Thomassen <86323303+LucyCiara@users.noreply.github.com>
Date: Fri, 20 Mar 2026 18:02:59 +0100
Subject: [PATCH 03/13] fix&test[model]: fix the repositories not only adding
new rows
---
.../edu/group5/app/model/DBRepository.java | 24 ++---
.../model/donation/DonationRepository.java | 87 +++++++++++--------
.../group5/app/model/user/UserRepository.java | 61 +++++++++----
.../donation/DonationRepositoryTest.java | 9 ++
.../app/model/user/UserRepositoryTest.java | 9 ++
5 files changed, 126 insertions(+), 64 deletions(-)
diff --git a/src/main/java/edu/group5/app/model/DBRepository.java b/src/main/java/edu/group5/app/model/DBRepository.java
index f3363b7..3cd1fa9 100644
--- a/src/main/java/edu/group5/app/model/DBRepository.java
+++ b/src/main/java/edu/group5/app/model/DBRepository.java
@@ -1,18 +1,21 @@
package edu.group5.app.model;
+
import java.util.HashMap;
import java.util.Map;
import java.util.List;
+
/**
* Abstract base class for repositories that store their data
* in a database-related structure.
*
*
- * Extends {@link Repository} and specifies that the content
- * is stored as a {@link Map}.
+ * Extends {@link Repository} and specifies that the content
+ * is stored as a {@link Map}.
*
*/
public abstract class DBRepository extends Repository {
protected final Map contentLock;
+
/**
* Constructs a DBRepository with the given content.
*
@@ -23,19 +26,18 @@ protected DBRepository(Map content) {
this.contentLock = new HashMap<>();
}
- protected void updateContentLock() {
- synchronized (contentLock) {
- contentLock.clear();
- contentLock.putAll(this.content);
- }
- }
+ protected abstract void updateContentLock();
public abstract boolean addContent(V value);
/**
- * Exports the repository content as a list of Object arrays, where each array represents a row of data.
- * This method is intended for converting the repository content into a format suitable for database storage or export.
- * @return a List of Object arrays representing the repository content for database export
+ * Exports the repository content as a list of Object arrays, where each array
+ * represents a row of data.
+ * This method is intended for converting the repository content into a format
+ * suitable for database storage or export.
+ *
+ * @return a List of Object arrays representing the repository content for
+ * database export
*/
public abstract List export();
}
diff --git a/src/main/java/edu/group5/app/model/donation/DonationRepository.java b/src/main/java/edu/group5/app/model/donation/DonationRepository.java
index 6181460..a55ea74 100644
--- a/src/main/java/edu/group5/app/model/donation/DonationRepository.java
+++ b/src/main/java/edu/group5/app/model/donation/DonationRepository.java
@@ -15,7 +15,7 @@
* Repository class for Donation.
*
*
- * Extends {@link DBRepository} and manages Donation entities.
+ * Extends {@link DBRepository} and manages Donation entities.
*
*/
public class DonationRepository extends DBRepository {
@@ -23,10 +23,12 @@ public class DonationRepository extends DBRepository {
/**
* Constructs DonationRepository from a list of Object[] rows from the database.
- * @param rows List of Object[] representing donations from the DB.
- * Each row must have 6 elements:
- * [donationId, userId, organizationId, amount, date, paymentMethod]
- * @throws IllegalArgumentException if the input list is null or any row is invalid
+ *
+ * @param rows List of Object[] representing donations from the DB.
+ * Each row must have 6 elements:
+ * [donationId, userId, organizationId, amount, date, paymentMethod]
+ * @throws IllegalArgumentException if the input list is null or any row is
+ * invalid
*/
public DonationRepository(List rows) {
super(new HashMap<>());
@@ -35,12 +37,12 @@ public DonationRepository(List rows) {
}
this.content = new HashMap<>();
for (Object[] row : rows) {
- if (row == null || row.length != 6 ) {
+ if (row == null || row.length != 6) {
throw new IllegalArgumentException("Each row must contain exactly 6 elements");
}
int donationId = (int) row[0];
int customerId = (int) row[1];
- int organizationId = (int) row[2];
+ int organizationId = (int) row[2];
BigDecimal amount = (BigDecimal) row[3];
Timestamp date = (Timestamp) row[4];
String paymentMethod = (String) row[5];
@@ -48,23 +50,39 @@ public DonationRepository(List rows) {
Donation donation = new Donation(donationId, customerId, organizationId, amount, date, paymentMethod);
this.content.put(donationId, donation);
}
- super.updateContentLock();
+ this.updateContentLock();
+ }
+
+ @Override
+ protected void updateContentLock() {
+ synchronized (contentLock) {
+ this.contentLock.clear();
+ this.contentLock.putAll(this.content);
+ }
}
@Override
public List export() {
- return content.entrySet().stream()
- .sorted(Map.Entry.comparingByKey())
- .map(entry -> { Donation donation = entry.getValue();
- return new Object[] {
- donation.donationId(), donation.userId(),
- donation.organizationId(), donation.amount(),
- donation.date(), donation.paymentMethod()};})
- .toList();
+ Map output = new HashMap<>(this.content);
+ for (int i : super.contentLock.keySet()) {
+ output.remove(i);
+ }
+ this.updateContentLock();
+ return output.entrySet().stream()
+ .sorted(Map.Entry.comparingByKey())
+ .map(entry -> {
+ Donation donation = entry.getValue();
+ return new Object[] {
+ donation.donationId(), donation.userId(),
+ donation.organizationId(), donation.amount(),
+ donation.date(), donation.paymentMethod() };
+ })
+ .toList();
}
/**
* Retrieves a donation by its ID.
+ *
* @param donationId the ID of the donation to retrieve
* @return the Donation object with the specified ID, or null if not found
* @throws IllegalArgumentException if the donationId is not positive
@@ -77,10 +95,12 @@ public Donation getDonationById(int donationId) {
}
/**
- * Generates the next donation ID based on the current maximum ID in the repository.
+ * Generates the next donation ID based on the current maximum ID in the
+ * repository.
+ *
* @return the next donation ID to be used for a new donation
*/
- public int getNextDonationId() {
+ public int getNextDonationId() {
return content.keySet().stream().max(Integer::compareTo).orElse(0) + 1;
} /* TODO change this when data database is introduced */
@@ -91,22 +111,22 @@ public Map getAllDonations() {
/**
* Adds a new donation to the repository
*
- * The donation is stored using its {@code donationId} as the key.
- * If a donation with the same ID already exists, the donation
- * will not be added.
+ * The donation is stored using its {@code donationId} as the key.
+ * If a donation with the same ID already exists, the donation
+ * will not be added.
*
*
* @param donation the donation to add
* @return {@code true} if the donation was successfully added, and
- * {@code false} if a donation with the same ID already exists
+ * {@code false} if a donation with the same ID already exists
*/
@Override
public boolean addContent(Donation donation) {
if (donation == null) {
throw new IllegalArgumentException("Donation cannot be null");
}
- if (content.containsKey(donation.donationId())){
- return false;
+ if (content.containsKey(donation.donationId())) {
+ return false;
}
this.content.put(donation.donationId(), donation);
return true;
@@ -116,12 +136,12 @@ public boolean addContent(Donation donation) {
* Returns all donations sorted by date (ascending).
*
*
- * The returned map preserves the sorted order.
+ * The returned map preserves the sorted order.
*
*
* @return a new {@link HashMap} containing the donations sorted by date
*/
- public HashMap sortByDate(){
+ public HashMap sortByDate() {
return content.entrySet().stream()
.sorted(Map.Entry.comparingByValue(
Comparator.comparing(Donation::date)))
@@ -136,7 +156,7 @@ public HashMap sortByDate(){
* Returns all donations sorted by amount (ascending).
*
*
- * The returned map preserves the sorted order from lowest to highest amount.
+ * The returned map preserves the sorted order from lowest to highest amount.
*
*
* @return a new {@link HashMap} containing the donations sorted by amount.
@@ -149,12 +169,12 @@ public HashMap sortByAmount() {
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
- LinkedHashMap::new
- ));
+ LinkedHashMap::new));
}
/**
* Returns all donations associated with a specific organization.
+ *
* @param orgNumber the organization ID to filter by
* @return a map containing all donations that belong to the given organization
* @throws IllegalArgumentException if the orgNumber is not positive
@@ -165,17 +185,17 @@ public HashMap filterByOrganization(int orgNumber) {
}
return content.entrySet()
.stream()
- .filter(entry -> entry.getValue().organizationId() == orgNumber)
+ .filter(entry -> entry.getValue().organizationId() == orgNumber)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
- LinkedHashMap::new
- ));
+ LinkedHashMap::new));
}
/**
* Returns all donations made by a specific user.
+ *
* @param userId the user ID to filter by
* @return a map containing all donations that belong to the given user
* @throws IllegalArgumentException if the userId is not positive
@@ -190,7 +210,6 @@ public HashMap filterByUser(int userId) {
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
- LinkedHashMap::new
- ));
+ LinkedHashMap::new));
}
}
diff --git a/src/main/java/edu/group5/app/model/user/UserRepository.java b/src/main/java/edu/group5/app/model/user/UserRepository.java
index 692126f..193f433 100644
--- a/src/main/java/edu/group5/app/model/user/UserRepository.java
+++ b/src/main/java/edu/group5/app/model/user/UserRepository.java
@@ -6,11 +6,13 @@
import edu.group5.app.model.DBRepository;
-public class UserRepository extends DBRepository{
+public class UserRepository extends DBRepository {
public final static String ROLE_CUSTOMER = "Customer";
+
/**
* Constructs UserRepository using Hashmap,
* and extends the content from DBRepository.
+ *
* @param content the underlying map used to store users,
* where the key represents the user ID
*/
@@ -20,7 +22,7 @@ public UserRepository(List rows) {
throw new IllegalArgumentException("The list of rows cannot be null");
}
for (Object[] row : rows) {
- if (row == null || row.length != 6 ) {
+ if (row == null || row.length != 6) {
throw new IllegalArgumentException("Each row must contain exactly 6 elements");
}
int userId = (int) row[0];
@@ -38,28 +40,46 @@ public UserRepository(List rows) {
}
this.content.put(userId, user);
}
- super.updateContentLock();
+ this.updateContentLock();
}
+ @Override
+ protected void updateContentLock() {
+ synchronized (contentLock) {
+ this.contentLock.clear();
+ this.contentLock.putAll(this.content);
+ }
+ }
@Override
public List export() {
- return content.entrySet().stream()
- .sorted(Map.Entry.comparingByKey())
- .map(entry -> { User user = entry.getValue();
- return new Object[]{user.getUserId(), user.getRole(),
- user.getFirstName(), user.getLastName(),
- user.getEmail(), user.getPasswordHash()};})
+ Map output = new HashMap<>(this.content);
+ for (int i : contentLock.keySet()) {
+ output.remove(i);
+ }
+ this.updateContentLock();
+ return output.entrySet().stream()
+ .sorted(Map.Entry.comparingByKey())
+ .map(entry -> {
+ User user = entry.getValue();
+ return new Object[] { user.getUserId(), user.getRole(),
+ user.getFirstName(), user.getLastName(),
+ user.getEmail(), user.getPasswordHash() };
+ })
.toList();
+
}
public HashMap getUsers() {
return new HashMap<>(content);
}
+
/**
* Retrieves a user by their unique identifier.
+ *
* @param userId the unique identifier of the user to retrieve
- * @return the user with the specified ID, or {@code null} if no such user exists
+ * @return the user with the specified ID, or {@code null} if no such user
+ * exists
* @throws IllegalArgumentException if the userId is not positive
*/
public User getUserById(int userId) {
@@ -72,12 +92,13 @@ public User getUserById(int userId) {
/**
* Generates the next user ID based on repository size.
* Uses size+1 and then moves forward if that ID is already taken.
+ *
* @return the next available user ID
* @throws IllegalStateException if no available user ID can be found
*/
- public int getNextUserId() {
+ public int getNextUserId() {
if (content.isEmpty()) {
- return 1;
+ return 1;
}
int maxKey = content.keySet().stream().max(Integer::compareTo).orElseThrow(
() -> new IllegalStateException("No keys found"));
@@ -88,21 +109,21 @@ public int getNextUserId() {
/**
* Adds a new user to the repository
*
- * The user is stored using its {@code userId} as the key.
- * If a user with the same ID already exists, the user
- * will not be added.
+ * The user is stored using its {@code userId} as the key.
+ * If a user with the same ID already exists, the user
+ * will not be added.
*
*
* @param user the user to add
* @return {@code true} if the user was successfully added, and
- * {@code false} if a user with the same ID already exists
+ * {@code false} if a user with the same ID already exists
*/
@Override
public boolean addContent(User user) {
if (user == null) {
throw new IllegalArgumentException("User cannot be null");
}
- if (content.containsKey(user.getUserId())){
+ if (content.containsKey(user.getUserId())) {
return false;
}
this.content.put(user.getUserId(), user);
@@ -111,8 +132,10 @@ public boolean addContent(User user) {
/**
* Finds a user by their email address.
+ *
* @param email the email address of the user to find
- * @return the user with the specified email address, or {@code null} if no such user exists
+ * @return the user with the specified email address, or {@code null} if no such
+ * user exists
*/
public User findUserByEmail(String email) {
if (email == null || email.trim().isEmpty()) {
@@ -122,5 +145,5 @@ public User findUserByEmail(String email) {
.filter(user -> user.getEmail().equals(email))
.findFirst()
.orElse(null);
- }
+ }
}
diff --git a/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java b/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java
index bdf0110..ce81b2a 100644
--- a/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java
+++ b/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java
@@ -263,4 +263,13 @@ void filterByUserIdThrowsIfNegative() {
() -> repo.filterByUser(0));
assertEquals("User ID must be positive", ex.getMessage());
}
+
+ @Test
+ void exportExportsOnlyNewInformation() {
+ repo.addContent(donation1);
+ repo.addContent(donation2);
+ assertEquals(2, repo.export().size());
+ repo.addContent(donation3);
+ assertEquals(1, repo.export().size());
+ }
}
\ No newline at end of file
diff --git a/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java b/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java
index 0700828..a4afbc8 100644
--- a/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java
+++ b/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java
@@ -12,6 +12,7 @@ public class UserRepositoryTest {
private UserRepository repo;
private List rows;
+private User additionalUser;
@BeforeEach
void setUp() {
@@ -19,6 +20,7 @@ void setUp() {
rows.add(new Object[]{1, "Customer", "John", "Cena", "john@example.com", "hashedpass"});
rows.add(new Object[]{2, "Customer", "Jane", "Doe", "jane@example.com", "hashedpass"});
repo = new UserRepository(rows);
+ this.additionalUser = new Customer(3, "John", "Doe", "john@example.com", "hashedpass");
}
@Test
@@ -145,4 +147,11 @@ void exportContainsAllUsers() {
assertEquals(2, exported.get(1)[0]);
assertEquals("Customer", exported.get(0)[1]);
}
+
+ @Test
+ void exportExportsOnlyNewInformation() {
+ assertEquals(0, repo.export().size());
+ repo.addContent(this.additionalUser);
+ assertEquals(1, repo.export().size());
+ }
}
\ No newline at end of file
From 8676d9bfdc2ba66d90f2d40f598da984cd6557f3 Mon Sep 17 00:00:00 2001
From: Fredrik Marjoni
Date: Fri, 20 Mar 2026 18:14:05 +0100
Subject: [PATCH 04/13] fix[DBWrapper]: Fix exporting of Donations to
successfully export when closing the application
---
.../java/edu/group5/app/control/wrapper/DbWrapper.java | 10 ++++++++--
.../app/control/wrapper/DbWrapperDonationsTest.java | 8 ++++++++
2 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java
index babb929..7e3adc0 100644
--- a/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java
+++ b/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java
@@ -95,10 +95,13 @@ public List importUsers() {
public int exportUsers(List data) {
this.importUsers();
-
+
if (data == null) {
throw new IllegalArgumentException("data can't be null");
}
+ if (data.isEmpty()) {
+ return 0;
+ }
if (data.get(0).length != 6) {
throw new IllegalArgumentException("data's arrays must have a length of 6");
}
@@ -184,10 +187,13 @@ private List importDonations(int user_id, boolean all) {
public int exportDonations(List data) {
this.fetchAllDonations();
-
+
if (data == null) {
throw new IllegalArgumentException("data can't be null");
}
+ if (data.isEmpty()) {
+ return 0;
+ }
if (data.get(0).length != 6) {
throw new IllegalArgumentException("data's arrays must have a length of 6");
}
diff --git a/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java b/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java
index f31d57c..8c401c4 100644
--- a/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java
+++ b/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java
@@ -196,4 +196,12 @@ public void addingDonationListWithNullInRowThrowsExpectedException() {
assertTrue(this.db.disconnect());
assertEquals("One or more rows in data contains null values", exception.getMessage());
}
+
+ @Test
+ public void dataIsEmptyAfterExportingAndImportingEmptyList() {
+ assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0);
+ assertEquals(0, this.db.exportDonations(new ArrayList()));
+ assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0);
+ assertTrue(this.db.disconnect());
+ }
}
From d60c6d5e979054fd2b6e7e65df95f04fbef7d3c5 Mon Sep 17 00:00:00 2001
From: Fredrik Marjoni
Date: Fri, 20 Mar 2026 18:42:12 +0100
Subject: [PATCH 05/13] fix[user]: fix failing tests
---
.../app/model/user/UserRepositoryTest.java | 9 ---------
.../group5/app/model/user/UserServiceTest.java | 16 ++++++++++++++++
2 files changed, 16 insertions(+), 9 deletions(-)
diff --git a/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java b/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java
index a4afbc8..eaf2c2c 100644
--- a/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java
+++ b/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java
@@ -139,15 +139,6 @@ void getNextUserIdReturns1IfEmpty() {
assertEquals(1, emptyRepo.getNextUserId());
}
- @Test
- void exportContainsAllUsers() {
- List exported = repo.export();
- assertEquals(2, exported.size());
- assertEquals(1, exported.get(0)[0]);
- assertEquals(2, exported.get(1)[0]);
- assertEquals("Customer", exported.get(0)[1]);
- }
-
@Test
void exportExportsOnlyNewInformation() {
assertEquals(0, repo.export().size());
diff --git a/src/test/java/edu/group5/app/model/user/UserServiceTest.java b/src/test/java/edu/group5/app/model/user/UserServiceTest.java
index 8f56957..acec10b 100644
--- a/src/test/java/edu/group5/app/model/user/UserServiceTest.java
+++ b/src/test/java/edu/group5/app/model/user/UserServiceTest.java
@@ -149,4 +149,20 @@ void loginInvalidEmail() {
assertNull(result2);
assertNull(result3);
}
+
+ @Test
+ void getUserByEmailValid() {
+ User result = service.getUserByEmail("jane.doe@example.com");
+ assertEquals("Jane", result.getFirstName());
+ }
+
+ @Test
+ void getUserByEmailNotFound() {
+ User result = service.getUserByEmail("");
+ User result2 = service.getUserByEmail(null);
+ User result3 = service.getUserByEmail(" ");
+ assertNull(result);
+ assertNull(result2);
+ assertNull(result3);
+ }
}
\ No newline at end of file
From d6ce27270ed15bdc68881b9c152ef238d84089b1 Mon Sep 17 00:00:00 2001
From: MatheaGjerde
Date: Tue, 24 Mar 2026 19:18:32 +0100
Subject: [PATCH 06/13] added jsoup dependency
---
pom.xml | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/pom.xml b/pom.xml
index f9a8b88..75290fc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -68,6 +68,11 @@
2.2.224
runtime
+
+ org.jsoup
+ jsoup
+ 1.17.2
+
From 7680c99e08b6880f72239f4ff6d3f96f37678ccc Mon Sep 17 00:00:00 2001
From: MatheaGjerde
Date: Tue, 24 Mar 2026 19:19:50 +0100
Subject: [PATCH 07/13] feat: added logoUrl atribute
---
.../group5/app/model/organization/Organization.java | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/src/main/java/edu/group5/app/model/organization/Organization.java b/src/main/java/edu/group5/app/model/organization/Organization.java
index 42844e1..7016567 100644
--- a/src/main/java/edu/group5/app/model/organization/Organization.java
+++ b/src/main/java/edu/group5/app/model/organization/Organization.java
@@ -7,7 +7,8 @@
*
*
* An organization is identified by an organization number, a name,
- * trust status, website Url, pre-approval status, and a textual description.
+ * trust status, website Url, pre-approval status, and a textual description,
+ * and a logo URL.
*
*
* Instances are validated on creation:
@@ -15,6 +16,7 @@
*
orgNumber must be non-negative
* name and websiteUrl must not be null or blank
* description must not be null
+ * logoUrl may be null if no logo is available
*
*/
public record Organization(
@@ -23,7 +25,8 @@ public record Organization(
boolean trusted,
String websiteUrl,
boolean isPreApproved,
- String description) {
+ String description,
+ String logoUrl) {
/**
* Creates a new organization.
*
@@ -35,12 +38,13 @@ public record Organization(
* @param isPreApproved whether the organization is pre-approved
* @param description a textual description of the organization; must not be
* null
+ * @param logoUrl the URL to the organization's logo image; may be null
* @throws NullPointerException if name, websiteUrl or description is null
* @throws IllegalArgumentException if orgNumber is negative, or if name or
* websiteUrl is blank
*/
public Organization(int orgNumber, String name, boolean trusted, String websiteUrl, boolean isPreApproved,
- String description) {
+ String description, String logoUrl) {
if (orgNumber < 0) {
throw new IllegalArgumentException("orgNumber cannot be negative");
}
@@ -50,6 +54,7 @@ public Organization(int orgNumber, String name, boolean trusted, String websiteU
this.websiteUrl = Objects.requireNonNull(websiteUrl, "websiteUrl cannot be null");
this.isPreApproved = isPreApproved;
this.description = Objects.requireNonNull(description, "description cannot be null");
+ this.logoUrl = logoUrl;
if (name.isBlank()) {
throw new IllegalArgumentException("name cannot be blank");
From f1488757d04e17e622f3d1e298dbb6b4d3be1983 Mon Sep 17 00:00:00 2001
From: MatheaGjerde
Date: Tue, 24 Mar 2026 19:23:53 +0100
Subject: [PATCH 08/13] feat: set logoUrl as null
---
.../group5/app/model/organization/OrganizationRepository.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java
index a47b3d5..cc0a6b1 100644
--- a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java
+++ b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java
@@ -43,7 +43,7 @@ public OrganizationRepository(Object[] input) {
String websiteURL = (String) contentMap.get("url");
boolean isPreApproved = Boolean.TRUE.equals(contentMap.get("is_pre_approved"));
String description = "Information about " + name;
- Organization org = new Organization(orgNumber, name, trusted, websiteURL, isPreApproved, description);
+ Organization org = new Organization(orgNumber, name, trusted, websiteURL, isPreApproved, description, null);
grandMap.put(org.orgNumber(), org);
}
From 632bdcb871be1777ca59ab1b12bafefe5c6ac562 Mon Sep 17 00:00:00 2001
From: MatheaGjerde
Date: Tue, 24 Mar 2026 19:25:06 +0100
Subject: [PATCH 09/13] feat: added methods to get organization logos from API
---
.../organization/OrganizationService.java | 91 +++++++++++++++++++
1 file changed, 91 insertions(+)
diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java
index c5979f5..9785040 100644
--- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java
+++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java
@@ -1,15 +1,28 @@
package edu.group5.app.model.organization;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+
+import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.CompletableFuture;
/**
* Service class for managing organization-related operations.
* It interacts with the OrganizationRepository to retrieve organization information
* and contains business logic associated with organization management.
+ *
+ * It provides fetching logo URLs by web scraping each organization's page on
+ * Innsamlingskontrollen.
+ *
+ * Fetched logo URLs are cached to avoid redundant network requests.
*/
public class OrganizationService {
private OrganizationRepository organizationRepository;
+ private final Map logoCache = new HashMap<>();
+
/**
* Constructs an OrganizationService with the given OrganizationRepository.
* @param organizationRepository the OrganizationRepository to use for managing organization data; must not be null
@@ -55,4 +68,82 @@ public Organization findByOrgNumber(int orgNumber) {
public Organization findByOrgName(String name) {
return organizationRepository.findByOrgName(name);
}
+
+ /**
+ * Fetches the logo URL for the given page by scraping the {@code div.logo img}
+ * element. Results are cached so each URL is only fetched once.
+ *
+ *
+ * Using Jsoup to web scrape through the URLs in the API.
+ *
+ * @param pageUrl the URL for the organization's page; may be null or blank
+ * @return the absolute logo URL, or null if not found or pageUrl is invalid
+ */
+
+ public String fetchLogoUrl(String pageUrl) {
+ if (pageUrl == null || pageUrl.isBlank()) {
+ return null;
+ }
+
+ if (logoCache.containsKey(pageUrl)) {
+ return logoCache.get(pageUrl);
+ }
+
+ try {
+ Document doc = Jsoup.connect(pageUrl).get();
+ Element img = doc.selectFirst("div.logo img");
+
+ if (img != null) {
+ String logoUrl = img.absUrl("src");
+ logoCache.put(pageUrl, logoUrl);
+ return logoUrl;
+ }
+ } catch (Exception e) {
+ System.out.println("Could not get logo for: " + pageUrl);
+ }
+ return null;
+ }
+
+ /**
+ * Fetches all trusted organizations with their logo URLs.
+ *
+ *
+ * For each trusted organization, attempts to get its logo using
+ * {@link #fetchLogoUrl(String)}. Creates a new Organization
+ * object including the logo URL.
+ *
+ * @return a map of trusted organizations keyed by organization number, with logos included
+ */
+ public Map getTrustedOrganizationsWithLogos() {
+ Map original = getTrustedOrganizations();
+ Map trustedOrgsWithLogos = new HashMap<>();
+
+ for (Organization org : original.values()) {
+ String logoUrl = fetchLogoUrl(org.websiteUrl());
+
+ Organization newOrg = new Organization(
+ org.orgNumber(),
+ org.name(),
+ org.trusted(),
+ org.websiteUrl(),
+ org.isPreApproved(),
+ org.description(),
+ logoUrl
+ );
+ trustedOrgsWithLogos.put(newOrg.orgNumber(), newOrg);
+ }
+ return trustedOrgsWithLogos;
+ }
+
+ /**
+ * Asynchronously fetches trusted organizations with logos.
+ *
+ * Runs in the background so the UI thread is no blocked.
+ * Returns a CompletableFuture that completes when all logos are loaded.
+ *
+ * @return a CompletableFuture containing a map of organizations with logos
+ */
+ public CompletableFuture> getTrustedOrganizationsWithLogosAsync() {
+ return CompletableFuture.supplyAsync(this::getTrustedOrganizationsWithLogos);
+ }
}
From 04c92c48946d270736565fb4534cc3ab623e596f Mon Sep 17 00:00:00 2001
From: MatheaGjerde
Date: Tue, 24 Mar 2026 19:26:22 +0100
Subject: [PATCH 10/13] feat: added images to BrowseCards
---
.../app/view/browsepage/BrowseCard.java | 28 +++++++++++++------
1 file changed, 20 insertions(+), 8 deletions(-)
diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java
index ad4be8a..b529e89 100644
--- a/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java
+++ b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java
@@ -37,20 +37,32 @@ public BrowseCard(BrowseCardController browseCardController, Organization org, S
private StackPane imageContainer(String img) {
StackPane imageContainer = new StackPane();
imageContainer.setId("imageContainer");
+
imageContainer.setPrefHeight(80);
imageContainer.setPrefWidth(80);
imageContainer.setMaxWidth(Double.MAX_VALUE);
- ImageView logo = new ImageView(
- new Image(getClass().getResource(img).toExternalForm())
- );
- logo.setId("logo");
- logo.setSmooth(true);
- logo.setPreserveRatio(true);
- logo.setFitHeight(80);
+ if (img != null && !img.isBlank()) {
+ ImageView logo = new ImageView(new Image(img, true));
+ logo.setId("logo");
+ logo.setSmooth(true);
+ logo.setPreserveRatio(true);
+ logo.setFitHeight(80);
+ logo.setFitWidth(80);
+
+ imageContainer.getChildren().add(logo);
+ } else {
+ StackPane placeholder = new StackPane();
+ placeholder.setPrefSize(80, 80);
+
+ Text text = new Text("No image");
+ text.setStyle("-fx-font-size: 10;");
+
+ placeholder.getChildren().add(text);
+ imageContainer.getChildren().add(placeholder);
+ }
- imageContainer.getChildren().add(logo);
return imageContainer;
}
From 27aff0fc1728738e38df5f2d5cd8625f7193c63e Mon Sep 17 00:00:00 2001
From: MatheaGjerde
Date: Tue, 24 Mar 2026 19:28:18 +0100
Subject: [PATCH 11/13] feat: added images to BrowsePageView
---
.../app/view/browsepage/BrowsePageView.java | 35 ++++++++++++++-----
1 file changed, 26 insertions(+), 9 deletions(-)
diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java b/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java
index a2d92c8..b27b011 100644
--- a/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java
+++ b/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java
@@ -6,6 +6,7 @@
import edu.group5.app.control.MainController;
import edu.group5.app.model.organization.Organization;
import edu.group5.app.view.Header;
+import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
@@ -75,8 +76,26 @@ private GridPane createOrganizationSection(String searchTerm) {
grid.setStyle("-fx-padding: 0;");
grid.setMaxWidth(Double.MAX_VALUE);
+ // Store reference for later updates
+ if (organizationGrid == null) {
+ organizationGrid = grid;
+ }
+
if (allOrganizations == null) {
- allOrganizations = mainController.getOrganizationService().getTrustedOrganizations();
+
+ //Show loading text while organizations and logos are fetched
+ grid.add(new javafx.scene.control.Label("Loading..."), 0, 0);
+
+ //Fetch trusted organizations with logos asynchronously (runs in background)
+ mainController.getOrganizationService()
+ .getTrustedOrganizationsWithLogosAsync()
+ .thenAccept(orgs -> {
+ this.allOrganizations = orgs;
+
+ // Update UI when data is ready
+ Platform.runLater(() -> updateOrganizationGrid(""));
+ });
+ return grid;
}
// Filter organizations by search term
@@ -86,13 +105,16 @@ private GridPane createOrganizationSection(String searchTerm) {
int row = 0;
for (Organization org : organizations.values()) {
- String defaultImg = "/browsepage/images/children_of_shambala.png";
- BrowseCard card = new BrowseCard(orgController, org, defaultImg);
+ //Adds default text if organization does not have any
+ String img = (org.logoUrl() != null && !org.logoUrl().isBlank())
+ ? org.logoUrl()
+ : null;
+
+ BrowseCard card = new BrowseCard(orgController, org, img);
grid.add(card, column, row);
column++;
-
if (column == 4) {
column = 0;
row++;
@@ -105,11 +127,6 @@ private GridPane createOrganizationSection(String searchTerm) {
grid.getColumnConstraints().add(col);
}
- // Store reference for later updates
- if (organizationGrid == null) {
- organizationGrid = grid;
- }
-
return grid;
}
From 33484e3609cf2ed2215fea9b13d4f94d58380acd Mon Sep 17 00:00:00 2001
From: MatheaGjerde
Date: Tue, 24 Mar 2026 19:28:49 +0100
Subject: [PATCH 12/13] feat: added tests to Organization classes
---
.../OrganizationRepositoryTest.java | 6 ++--
.../organization/OrganizationServiceTest.java | 15 +++++++++
.../model/organization/OrganizationTest.java | 32 +++++++++++++++----
3 files changed, 44 insertions(+), 9 deletions(-)
diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java
index 7a5ece5..f821e35 100644
--- a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java
+++ b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java
@@ -73,7 +73,7 @@ void getTrustedOrganizations_OnlyReturnsTrustedOrganizations() {
@Test
void testFindByOrgNumberReturnsOrganization() {
assertEquals(new Organization(1, "Trusted Org1", true,
- "org.com", true, "Information about Trusted Org1"),
+ "org.com", true, "Information about Trusted Org1", null),
repository.findByOrgNumber(1));
}
@@ -93,7 +93,7 @@ void testFindByOrgNumberIfOrgNumberNotFound() {
@Test
void testFindByOrgNameReturnsOrganization() {
assertEquals(new Organization(1, "Trusted Org1", true,
- "org.com", true, "Information about Trusted Org1"),
+ "org.com", true, "Information about Trusted Org1", null),
repository.findByOrgName("Trusted Org1"));
}
@@ -116,7 +116,7 @@ void testFindByOrgNameIfNameNotFound() {
@Test
void testFindByOrgNameIsCaseInsensitive() {
assertEquals(new Organization(1, "Trusted Org1", true,
- "org.com", true, "Information about Trusted Org1"),
+ "org.com", true, "Information about Trusted Org1", null),
repository.findByOrgName("trusted org1"));
}
diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java
index e34aba7..0920e67 100644
--- a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java
+++ b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java
@@ -67,4 +67,19 @@ void testFindByOrgName() {
assertEquals(1, org.orgNumber());
assertEquals("Misjonsalliansen", org.name());
}
+
+ @Test
+ void fetchLogoUrlReturnsNullWhenUrlIsNull() {
+ assertNull(service.fetchLogoUrl(null));
+ }
+ @Test
+ void fetchLogoUrlReturnsNullWhenUrlIsBlank() {
+ assertNull(service.fetchLogoUrl(""));
+ }
+ @Test
+ void fetchLogoUrlCachesResultOnSecondCall() {
+ String result1 = service.fetchLogoUrl("https://");
+ String result2 = service.fetchLogoUrl("https://");
+ assertEquals(result1, result2);
+ }
}
diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationTest.java
index f921b60..0b97840 100644
--- a/src/test/java/edu/group5/app/model/organization/OrganizationTest.java
+++ b/src/test/java/edu/group5/app/model/organization/OrganizationTest.java
@@ -14,7 +14,8 @@ void constructor_CreatesAnOrganizationWhenInputIsValid() {
true,
"org.com",
true,
- "Org description"
+ "Org description",
+ null
);
assertAll(
@@ -35,7 +36,8 @@ void constructor_ThrowsWhenOrgNumberIsNegative() {
true,
"org.com",
true,
- "Org description"
+ "Org description",
+ null
));
}
@@ -47,7 +49,8 @@ void constructor_ThrowsWhenNameIsNull() {
true,
"org.com",
true,
- "Org description"
+ "Org description",
+ null
));
}
@@ -59,7 +62,8 @@ void constructor_ThrowsWhenNameIsBlank() {
true,
"org.com",
true,
- "Org description"
+ "Org description",
+ null
));
}
@@ -71,7 +75,8 @@ void constructor_ThrowsWhenWebsiteURLIsNull() {
true,
null,
true,
- "Org description"
+ "Org description",
+ null
));
}
@@ -83,7 +88,8 @@ void constructor_ThrowsWhenWebsiteURLIsBlank() {
true,
"",
true,
- "Org description"
+ "Org description",
+ null
));
}
@@ -95,6 +101,20 @@ void constructor_ThrowsWhenDescriptionIsNull() {
true,
"org.com",
true,
+ null,
+ null
+ ));
+ }
+
+ @Test
+ void constructor_AcceptsNullLogoUrl() {
+ assertDoesNotThrow(() -> new Organization(
+ 1,
+ "Org",
+ true,
+ "org.com",
+ true,
+ "description",
null
));
}
From 5db4ec6270743fce4e4754cb8fe89e5daa6e2eb1 Mon Sep 17 00:00:00 2001
From: MatheaGjerde
Date: Tue, 24 Mar 2026 21:02:22 +0100
Subject: [PATCH 13/13] feat: added images to Organization pages
---
.../OrganizationPageView.java | 26 +++++++++++--------
1 file changed, 15 insertions(+), 11 deletions(-)
diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java
index 4ec7c91..b0afa27 100644
--- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java
+++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java
@@ -63,17 +63,21 @@ private StackPane createImageContainer() {
imageContainer.setMaxWidth(Double.MAX_VALUE);
Organization org = mainController.getCurrentOrganization();
- String imagePath = org != null ? "/browsepage/images/children_of_shambala.png" : "/browsepage/images/children_of_shambala.png";
-
- ImageView logo = new ImageView(
- new Image(getClass().getResource(imagePath).toExternalForm())
- );
-
- logo.setId("logo");
- logo.setSmooth(true);
- logo.setPreserveRatio(true);
-
- imageContainer.getChildren().add(logo);
+ if (org != null && org.logoUrl() != null && !org.logoUrl().isBlank()) {
+ ImageView logo = new ImageView(new Image(org.logoUrl(), true));
+ logo.setId("logo");
+ logo.setSmooth(true);
+ logo.setPreserveRatio(true);
+ imageContainer.getChildren().add(logo);
+ } else {
+ StackPane placeholder = new StackPane();
+
+ Text text = new Text("No image");
+ text.setStyle("-fx-font-size: 10;");
+
+ placeholder.getChildren().add(text);
+ imageContainer.getChildren().add(placeholder);
+ }
return imageContainer;
}