From f64ebc41707fdddf35268a37b4e52b86ec1440a4 Mon Sep 17 00:00:00 2001 From: Roar Date: Tue, 24 Feb 2026 14:09:14 +0100 Subject: [PATCH 001/123] Delete Classes. Deleted non-relevant classes for web-scraping objects/methods. --- .../java/ntnu/sytemutvikling/team6/Main.java | 7 -- .../sytemutvikling/team6/models/Charity.java | 98 ------------------- .../team6/models/CharityRegistry.java | 0 .../sytemutvikling/team6/models/Donation.java | 79 --------------- .../team6/models/DonationRegistry.java | 5 - .../sytemutvikling/team6/models/Feedback.java | 65 ------------ .../sytemutvikling/team6/models/Message.java | 0 .../sytemutvikling/team6/models/Settings.java | 5 - .../team6/models/UserRegistry.java | 6 -- .../team6/service/AuthenticationService.java | 4 - .../team6/service/CharityService.java | 4 - .../team6/service/DonationService.java | 4 - .../team6/service/FeedbackService.java | 4 - 13 files changed, 281 deletions(-) delete mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/Main.java delete mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Charity.java delete mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/CharityRegistry.java delete mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Donation.java delete mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DonationRegistry.java delete mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Feedback.java delete mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Message.java delete mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Settings.java delete mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/UserRegistry.java delete mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/AuthenticationService.java delete mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/CharityService.java delete mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/DonationService.java delete mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/FeedbackService.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/Main.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/Main.java deleted file mode 100644 index d265ebe..0000000 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/Main.java +++ /dev/null @@ -1,7 +0,0 @@ -package ntnu.sytemutvikling.team6; - -public class Main { - public static void main(String[] args) { - System.out.println("Hello world!"); - } -} \ No newline at end of file diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Charity.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Charity.java deleted file mode 100644 index 4a389f4..0000000 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Charity.java +++ /dev/null @@ -1,98 +0,0 @@ -/** - * This class represents a charity organization. It contains information about the charity such as its name, description, total donations, verification status, and category. - * - * @author Adrian Balunan - */ -package ntnu.sytemutvikling.team6.models; - -import java.util.List; -import java.util.UUID; -import java.util.ArrayList; - -abstract class Charity { - /* UUID for uniquely identifying each charity */ - private UUID id; - - /* Name of the charity */ - private String name; - - /* Description of the charity's mission and activities */ - private String description; - - /* Total Donations received */ - private int totalDonations; - - /* Is the charity verified? */ - private boolean isVerified; - - /* Category for the charity */ - private String category; - - /* List that contains the charity's Feedbacks */ - private List feedbacks; - - /** - * Konstructor for creating a new charity. - * The ID is generated automatically using UUID. - * Total donations are initialized to 0. - * The charity is unverified by default. - * - * @param name - * @param description - * @param category - */ - public Charity(String name, String description, String category) { - this.id = UUID.randomUUID(); - this.name = name; - this.description = description; - this.totalDonations = 0; - this.isVerified = false; - this.feedbacks = new ArrayList<>(); - this.category = category; - } - - /** - * Getters for the charity's attributes. - */ - public UUID getId() { - return id; - } - public String getCategory() { - return category; - } - public String getName() { - return name; - } - public String getDescription() { - return description; - } - public int getTotalDonations() { - return totalDonations; - } - public boolean isVerified() { - return isVerified; - } - - /** - * Setter for verification status. - * This one sets the charity as verified. - */ - public void setVerified() { - this.isVerified = true; - } - - /** - * Setter for verification status. - * This one sets the charity as unverified. - */ - public void setUnverified() { - this.isVerified = false; - } - - /** - * Setter for total donations. This method is used to update the total donations when a new donation is made. - */ - public void setTotalDonations(int amount) { - this.totalDonations += amount; - } -} diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/CharityRegistry.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/CharityRegistry.java deleted file mode 100644 index e69de29..0000000 diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Donation.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Donation.java deleted file mode 100644 index 78569b2..0000000 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Donation.java +++ /dev/null @@ -1,79 +0,0 @@ -package ntnu.sytemutvikling.team6.models; - -import java.time.LocalDateTime; -import java.util.UUID; - -public class Donation { - /* UUID for uniquely identifying each donation */ - private UUID charityId; - - /* Ammount of money donated */ - private double amount; - - /* Date and time of the donation */ - private LocalDateTime date; - - /* The charity that received the donation */ - private Charity charity; - - /* The user/donor that made the donation */ - private User donor; - - /** - * Is the donation made anonymously? - * This can be null if the donation was made anonymously. - * - */ - private boolean isAnonymous; - - /** - * Constructor for creating a new donation. - * The charityId is generated automatically using UUID. - * If the donation is made anonymously, the isAnonymous parameter is set to true. - * @param amount - * @param date - * @param charity - * @param donor - */ - public Donation(double amount, LocalDateTime date, Charity charity, User donor) { - this.charityId = UUID.randomUUID(); - this.amount = amount; - this.date = date; - this.charity = charity; - this.donor = donor; - - - // ASSUMES that this is the way to get the anonymous setting from the user's settings. - if (donor.getSettings().getAnonymous() == false) { - this.isAnonymous = true; - } else { - this.isAnonymous = false; - - } - } - - /* Getters for the donation's attributes */ - public boolean isAnonymous() { - return isAnonymous; - } - - public UUID getCharityId() { - return charityId; - } - - public double getAmount() { - return amount; - } - - public LocalDateTime getDate() { - return date; - } - - public Charity getCharity() { - return charity; - } - - public User getDonor() { - return donor; - } -} diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DonationRegistry.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DonationRegistry.java deleted file mode 100644 index f565298..0000000 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DonationRegistry.java +++ /dev/null @@ -1,5 +0,0 @@ -package ntnu.sytemutvikling.team6.models; - -public class DonationRegistry { - -} diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Feedback.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Feedback.java deleted file mode 100644 index 891dce1..0000000 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Feedback.java +++ /dev/null @@ -1,65 +0,0 @@ -package ntnu.sytemutvikling.team6.models; - -import java.time.LocalDateTime; -import java.util.UUID; - -public class Feedback { - /* Feedback id */ - private UUID feedbackId; - - /** - * The author of the feedback - * If annonymous the presentation of the user will be "Anonymous". - */ - private User user; - - /* The details of the feedback*/ - private String comment; - - /* The date and time when the feedback was given */ - private LocalDateTime date; - - /* Is the feedback given anonymously? */ - private boolean isAnonymous; - - /** - * Constructor for creating a new feedback. - * - * @param user The user who gives the feedback. - * @param comment The content of the feedback. - */ - public Feedback(User user, String comment) { - this.feedbackId = UUID.randomUUID(); - this.user = user; - this.comment = comment; - this.date = LocalDateTime.now(); - - // ASSUMES that this is the way to get the anonymous setting from the user's settings. - if (user.getSettings().getAnonymous() == false) { - this.isAnonymous = true; - } else { - this.isAnonymous = false; - } - } - - /** - * Getters for the feedback's attributes. - * - * @return The feedback's attributes. - */ - public UUID getFeedbackId() { - return feedbackId; - } - public String getComment() { - return comment; - } - public LocalDateTime getDate() { - return date; - } - public User getUser() { - return user; - } - public boolean isAnonymous() { - return isAnonymous; - } -} diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Message.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Message.java deleted file mode 100644 index e69de29..0000000 diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Settings.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Settings.java deleted file mode 100644 index 71e218a..0000000 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Settings.java +++ /dev/null @@ -1,5 +0,0 @@ -package ntnu.sytemutvikling.team6.models; - -public class Settings { - -} diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/UserRegistry.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/UserRegistry.java deleted file mode 100644 index 7b97e02..0000000 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/UserRegistry.java +++ /dev/null @@ -1,6 +0,0 @@ -package ntnu.sytemutvikling.team6.models; - -public class UserRegistry { - - -} diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/AuthenticationService.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/AuthenticationService.java deleted file mode 100644 index 5f98c21..0000000 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/AuthenticationService.java +++ /dev/null @@ -1,4 +0,0 @@ -package ntnu.sytemutvikling.team6.service; - -public class AuthenticationService { -} diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/CharityService.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/CharityService.java deleted file mode 100644 index 6bd33ce..0000000 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/CharityService.java +++ /dev/null @@ -1,4 +0,0 @@ -package ntnu.sytemutvikling.team6.service; - -public class CharityService { -} diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/DonationService.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/DonationService.java deleted file mode 100644 index bc4a7c8..0000000 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/DonationService.java +++ /dev/null @@ -1,4 +0,0 @@ -package ntnu.sytemutvikling.team6.service; - -public class DonationService { -} diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/FeedbackService.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/FeedbackService.java deleted file mode 100644 index 93b2555..0000000 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/FeedbackService.java +++ /dev/null @@ -1,4 +0,0 @@ -package ntnu.sytemutvikling.team6.service; - -public class FeedbackService { -} From ebab1f258826e40c3122e042c6cc56522aa86940 Mon Sep 17 00:00:00 2001 From: Roar Date: Tue, 24 Feb 2026 14:09:51 +0100 Subject: [PATCH 002/123] Updated pom.xml Added selenium as a new dependency. --- helpmehelpapplication/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helpmehelpapplication/pom.xml b/helpmehelpapplication/pom.xml index da3d4e2..0ad37a5 100644 --- a/helpmehelpapplication/pom.xml +++ b/helpmehelpapplication/pom.xml @@ -25,6 +25,11 @@ javafx-controls 25.0.1 + + org.seleniumhq.selenium + selenium-java + 4.41.0 + From 4d145ae3784926601f4544cae5f67aa4bf64e9b9 Mon Sep 17 00:00:00 2001 From: Roar Date: Tue, 24 Feb 2026 14:14:16 +0100 Subject: [PATCH 003/123] Added IKOrganizationScraper class Added class to scrape confirmed organizations from InnsamlingsKontrollen. --- .../team6/models/IKOrganizationScraper.java | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java new file mode 100644 index 0000000..cbc4b0f --- /dev/null +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java @@ -0,0 +1,96 @@ +package ntnu.sytemutvikling.team6.models; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +public class IKOrganizationScraper { + private final List organizationData; + + public IKOrganizationScraper() { + this.organizationData = new ArrayList<>(); + } + + public boolean updateData() { + // Configure headless chrome browser + ChromeOptions options = new ChromeOptions(); + options.addArguments("--headless=new"); + options.addArguments("--window-size=1920,1080"); + options.addArguments("--disable-gpu"); + options.addArguments("--no-sandbox"); + options.addArguments("--disable-dev-shm-usage"); + + WebDriver driver = new ChromeDriver(options); + + //URL for godkjente organisasjoner + driver.get("https://www.innsamlingskontrollen.no/organisasjoner/"); + + // Wait to ensure tabular data is loaded first + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); + wait.until(ExpectedConditions.presenceOfElementLocated(By.tagName("table"))); + + // Loops through table rows and columns + List rows = driver.findElements(By.cssSelector("table tbody tr")); + + String name = null; + String telephone = null; + String location = null; + String status = null; + + // Clear old data before updating + this.organizationData.clear(); + + for (WebElement row : rows) { + List columns = row.findElements(By.tagName("td")); + + for (int i = 0; i < columns.size(); i++) { + + WebElement column = columns.get(i); + + // Non-verification columns + if (i == 0) { + name = column.getText(); + } + + if (i == 1) { + telephone = columns.get(i).getText(); + } + + if (i == 2) { + location = columns.get(i).getText(); + } + + // Verification column + if (i == 3) { + if (!column.findElements(By.cssSelector(".status-pre-approved")).isEmpty()) { + status = "Monitored"; + } else if (!column.findElements(By.cssSelector(".status-approved")).isEmpty()) { + status = "Approved"; + } else { + status = "Unknown"; + } + } + } + + var organization = new Organization(name, telephone, location, status); + this.organizationData.add(organization); + } + driver.quit(); + + // Removes the first empty row that lists only the categories, aka no relevant data + this.organizationData.removeFirst(); + + return true; + } + + public List getData() { + return this.organizationData; + } +} From bf7327e318c4a76c1648591a08f5cac44a136467 Mon Sep 17 00:00:00 2001 From: Roar Date: Tue, 24 Feb 2026 14:14:52 +0100 Subject: [PATCH 004/123] Added Organization class Added class to store values from scraped organizations. --- .../team6/models/Organization.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Organization.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Organization.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Organization.java new file mode 100644 index 0000000..600b7a9 --- /dev/null +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Organization.java @@ -0,0 +1,32 @@ +package ntnu.sytemutvikling.team6.models; + +public class Organization { + String name; + String telephone; + String location; + String status; + + public Organization(String name, String telephone, String location, String status) { + this.name = name; + this.telephone = telephone; + this.location = location; + this.status = status; + } + + public String getName() { + return this.name; + } + + public String getTelephone() { + return this.telephone; + } + + public String getLocation() { + return this.location; + } + + public String getStatus() { + return this.status; + } + +} From 7e7696a4186b910631edf38a2ffc617882b218b1 Mon Sep 17 00:00:00 2001 From: Roar Date: Tue, 24 Feb 2026 14:37:09 +0100 Subject: [PATCH 005/123] Updated IKOrganizationScraper Changed some of the logic in updateData() to prepare for format for writing to csv file. --- .../team6/models/IKOrganizationScraper.java | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java index cbc4b0f..c4abb75 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java @@ -36,9 +36,6 @@ public boolean updateData() { WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); wait.until(ExpectedConditions.presenceOfElementLocated(By.tagName("table"))); - // Loops through table rows and columns - List rows = driver.findElements(By.cssSelector("table tbody tr")); - String name = null; String telephone = null; String location = null; @@ -47,28 +44,38 @@ public boolean updateData() { // Clear old data before updating this.organizationData.clear(); - for (WebElement row : rows) { - List columns = row.findElements(By.tagName("td")); + // Loops through table rows and columns + List rows = driver.findElements(By.cssSelector("table tbody tr")); + + for (int i = 0; i < rows.size(); i++) { + List columns = rows.get(i).findElements(By.tagName("td")); - for (int i = 0; i < columns.size(); i++) { + // Create organization with category names for csv file + if (i == 0) { + var categories = new Organization("Name", "Telephone", "Location", "Status"); + this.organizationData.add(categories); + continue; + } - WebElement column = columns.get(i); + for (int j = 0; j < columns.size(); j++) { + + WebElement column = columns.get(j); // Non-verification columns - if (i == 0) { + if (j == 0) { name = column.getText(); } - if (i == 1) { - telephone = columns.get(i).getText(); + if (j == 1) { + telephone = columns.get(j).getText(); } - if (i == 2) { - location = columns.get(i).getText(); + if (j == 2) { + location = columns.get(j).getText(); } // Verification column - if (i == 3) { + if (j == 3) { if (!column.findElements(By.cssSelector(".status-pre-approved")).isEmpty()) { status = "Monitored"; } else if (!column.findElements(By.cssSelector(".status-approved")).isEmpty()) { @@ -84,9 +91,6 @@ public boolean updateData() { } driver.quit(); - // Removes the first empty row that lists only the categories, aka no relevant data - this.organizationData.removeFirst(); - return true; } From d6ed083914b2065ea15db5ae69b6197341797359 Mon Sep 17 00:00:00 2001 From: Roar Date: Thu, 26 Feb 2026 10:57:23 +0100 Subject: [PATCH 006/123] Updated pom.xml Added opencsv as a dependency. --- helpmehelpapplication/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helpmehelpapplication/pom.xml b/helpmehelpapplication/pom.xml index 0ad37a5..57397c7 100644 --- a/helpmehelpapplication/pom.xml +++ b/helpmehelpapplication/pom.xml @@ -30,6 +30,11 @@ selenium-java 4.41.0 + + com.opencsv + opencsv + 5.12.0 + From 07d0180dd3ee2c036ef403ba748a8cfe6c268d0c Mon Sep 17 00:00:00 2001 From: Roar Date: Thu, 26 Feb 2026 10:59:11 +0100 Subject: [PATCH 007/123] Updated IKOrganizationScraper Changed the logic for updateData to be more robust (old method didn't get all data reliably). Put method in a try - finally statement to ensure headless chrome browser reliably shuts down after completion/if error. Added a method to write gathered data to a csv file. Made the getter method return an immutable list. --- .../team6/models/IKOrganizationScraper.java | 154 +++++++++--------- 1 file changed, 78 insertions(+), 76 deletions(-) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java index c4abb75..d7e833d 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java @@ -1,8 +1,12 @@ package ntnu.sytemutvikling.team6.models; +import java.io.FileWriter; +import java.io.IOException; import java.time.Duration; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import com.opencsv.CSVWriter; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; @@ -12,89 +16,87 @@ import org.openqa.selenium.support.ui.WebDriverWait; public class IKOrganizationScraper { + private final List organizationData; + private final String filename = "charities.csv"; public IKOrganizationScraper() { this.organizationData = new ArrayList<>(); } - public boolean updateData() { - // Configure headless chrome browser - ChromeOptions options = new ChromeOptions(); - options.addArguments("--headless=new"); - options.addArguments("--window-size=1920,1080"); - options.addArguments("--disable-gpu"); - options.addArguments("--no-sandbox"); - options.addArguments("--disable-dev-shm-usage"); - - WebDriver driver = new ChromeDriver(options); - - //URL for godkjente organisasjoner - driver.get("https://www.innsamlingskontrollen.no/organisasjoner/"); - - // Wait to ensure tabular data is loaded first - WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); - wait.until(ExpectedConditions.presenceOfElementLocated(By.tagName("table"))); - - String name = null; - String telephone = null; - String location = null; - String status = null; - - // Clear old data before updating - this.organizationData.clear(); - - // Loops through table rows and columns - List rows = driver.findElements(By.cssSelector("table tbody tr")); - - for (int i = 0; i < rows.size(); i++) { - List columns = rows.get(i).findElements(By.tagName("td")); - - // Create organization with category names for csv file - if (i == 0) { - var categories = new Organization("Name", "Telephone", "Location", "Status"); - this.organizationData.add(categories); - continue; - } - - for (int j = 0; j < columns.size(); j++) { - - WebElement column = columns.get(j); - - // Non-verification columns - if (j == 0) { - name = column.getText(); - } - - if (j == 1) { - telephone = columns.get(j).getText(); - } - - if (j == 2) { - location = columns.get(j).getText(); - } - - // Verification column - if (j == 3) { - if (!column.findElements(By.cssSelector(".status-pre-approved")).isEmpty()) { - status = "Monitored"; - } else if (!column.findElements(By.cssSelector(".status-approved")).isEmpty()) { - status = "Approved"; - } else { - status = "Unknown"; - } + public boolean updateData() { + // Configure headless Chrome browser + ChromeOptions options = new ChromeOptions(); + options.addArguments("--headless=new"); + options.addArguments("--window-size=1920,1080"); + options.addArguments("--disable-gpu"); + options.addArguments("--no-sandbox"); + options.addArguments("--disable-dev-shm-usage"); + + WebDriver driver = new ChromeDriver(options); + + try { + //URL for scraping approved organizations + driver.get("https://www.innsamlingskontrollen.no/organisasjoner/"); + + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30)); + wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(By.cssSelector( + "table tbody tr"), 0)); + List rows = driver.findElements(By.cssSelector("table tbody tr")); + + if (!rows.isEmpty()) { + wait.until(ExpectedConditions.visibilityOf(rows.getLast())); + } + + // Clear old data + this.organizationData.clear(); + + // Add CSV header + this.organizationData.add(new Organization( + "Name", "Telephone", "Location", "Status")); + + // Loop through table rows + for (WebElement row : rows) { + List columns = row.findElements(By.tagName("td")); + if (columns.size() < 4) continue; + + String name = columns.get(0).getText(); + String telephone = columns.get(1).getText(); + String location = columns.get(2).getText(); + + WebElement statusColumn = columns.get(3); + + String status; + if (!statusColumn.findElements(By.cssSelector(".status-pre-approved")).isEmpty()) { + status = "Monitored"; + } else if (!statusColumn.findElements(By.cssSelector(".status-approved")).isEmpty()) { + status = "Approved"; + } else { + status = "Unknown"; + } + this.organizationData.add(new Organization(name, telephone, location, status)); + } + } finally { + driver.quit(); } - } - - var organization = new Organization(name, telephone, location, status); - this.organizationData.add(organization); + return true; } - driver.quit(); - return true; - } + public boolean writeToCSV() throws IOException { + try (CSVWriter writer = new CSVWriter(new FileWriter(filename))) { + for (Organization o : this.organizationData) { + writer.writeNext(new String[]{ + o.getName(), + o.getTelephone(), + o.getLocation(), + o.getStatus() + }); + } + } + return true; + } - public List getData() { - return this.organizationData; - } -} + public List getData() { + return Collections.unmodifiableList(this.organizationData); + } +} \ No newline at end of file From 9121be598097b1804afaecfe7b4f6a324adffc62 Mon Sep 17 00:00:00 2001 From: Roar Date: Thu, 26 Feb 2026 11:06:53 +0100 Subject: [PATCH 008/123] Updated IKOrganizationScraper Added method deleteCSV that deletes the csv if it exists. Added method to writeToCSV for cleaner generation of csv. Also added exceptions to handle missing data and failure to delete CSV file. --- .../team6/models/IKOrganizationScraper.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java index d7e833d..e43eef0 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java @@ -1,5 +1,6 @@ package ntnu.sytemutvikling.team6.models; +import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.time.Duration; @@ -83,6 +84,15 @@ public boolean updateData() { } public boolean writeToCSV() throws IOException { + if (this.organizationData.isEmpty()) { + throw new IllegalArgumentException("There is no data in the " + + "organizationData list. Run .updateData before proceeding."); + } + + if (!this.deleteCSV()) { + throw new IOException("Failed to delete the CSV file."); + } + try (CSVWriter writer = new CSVWriter(new FileWriter(filename))) { for (Organization o : this.organizationData) { writer.writeNext(new String[]{ @@ -96,6 +106,17 @@ public boolean writeToCSV() throws IOException { return true; } + public boolean deleteCSV() { + var file = new File(filename); + + // Returns true if file is deleted (or if file doesn't exist + // Returns false if file couldn't be deleted + if (file.exists()) { + return file.delete(); + } + return true; + } + public List getData() { return Collections.unmodifiableList(this.organizationData); } From 13c07eaf7ae9f882f4c7207e79d18572ae4a36c8 Mon Sep 17 00:00:00 2001 From: Roar Date: Thu, 26 Feb 2026 11:11:05 +0100 Subject: [PATCH 009/123] Updated Organization Changed parameters to private immutable parameters. --- .../ntnu/sytemutvikling/team6/models/Organization.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Organization.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Organization.java index 600b7a9..d6ab8bc 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Organization.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Organization.java @@ -1,10 +1,10 @@ package ntnu.sytemutvikling.team6.models; public class Organization { - String name; - String telephone; - String location; - String status; + private final String name; + private final String telephone; + private final String location; + private final String status; public Organization(String name, String telephone, String location, String status) { this.name = name; From 8b7adc28c6077bf273b4f9bc81f98c1e863f43cc Mon Sep 17 00:00:00 2001 From: Roar Date: Thu, 26 Feb 2026 11:28:04 +0100 Subject: [PATCH 010/123] Added OrganizationTest test class. Quick test class to check that the Organization object is created successfully with correct parameters. --- .../team6/models/OrganizationTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/OrganizationTest.java diff --git a/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/OrganizationTest.java b/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/OrganizationTest.java new file mode 100644 index 0000000..2a4d5d6 --- /dev/null +++ b/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/OrganizationTest.java @@ -0,0 +1,24 @@ +package ntnu.sytemutvikling.team6.models; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class OrganizationTest { + + @Test + void initialParametersShouldBeCorrect() { + String name = "Hjelpesenter"; + String telephone = "123456789"; + String location = "London"; + String status = "Approved"; + + var org = new Organization(name, telephone, location, status); + + assertEquals(name, org.getName(), "Name parameter should be correct."); + assertEquals(telephone, org.getTelephone(), "Telephone parameter should be correct."); + assertEquals(location, org.getLocation(), "Location parameter should be correct."); + assertEquals(status, org.getStatus(), "Status parameter should be correct."); + } + +} \ No newline at end of file From 675612f0b10f652d2cbf03c9f1ef3e5f74b65341 Mon Sep 17 00:00:00 2001 From: Roar Date: Fri, 27 Feb 2026 09:30:17 +0100 Subject: [PATCH 011/123] Updated pom.xml Added GSON as a Maven dependency. --- helpmehelpapplication/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helpmehelpapplication/pom.xml b/helpmehelpapplication/pom.xml index 57397c7..4a67e62 100644 --- a/helpmehelpapplication/pom.xml +++ b/helpmehelpapplication/pom.xml @@ -35,6 +35,11 @@ opencsv 5.12.0 + + com.google.code.gson + gson + 2.13.2 + From f26ab51fac4a55eba04b028716c05fc4dcd7cdc8 Mon Sep 17 00:00:00 2001 From: Roar Date: Fri, 27 Feb 2026 10:20:38 +0100 Subject: [PATCH 012/123] Added APICharityScraper & APICharityData class. Added a class for scraping the IK API and parsing the JSON values, pushing them to a APICharityData object for easy migrating to a database later. --- .../team6/models/APICharityData.java | 37 +++++++++++ .../team6/models/APICharityScraper.java | 63 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityData.java create mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityData.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityData.java new file mode 100644 index 0000000..d94db88 --- /dev/null +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityData.java @@ -0,0 +1,37 @@ +package ntnu.sytemutvikling.team6.models; + +public class APICharityData { + private final String org_number; + private final String name; + private final String status; + private final String url; + private final boolean is_pre_approved; + + public APICharityData(String org_number, String name, String status, String url, boolean is_pre_approved) { + this.org_number = org_number; + this.name = name; + this.status = status; + this.url = url; + this.is_pre_approved = is_pre_approved; + } + + public String getOrg_number() { + return this.org_number; + } + + public String getName() { + return name; + } + + public String getStatus() { + return status; + } + + public String getUrl() { + return url; + } + + public boolean getIs_pre_approved() { + return this.is_pre_approved; + } +} diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java new file mode 100644 index 0000000..27f2e60 --- /dev/null +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java @@ -0,0 +1,63 @@ +package ntnu.sytemutvikling.team6.models; + +import com.google.gson.Gson; + +import java.io.IOException; +import java.net.*; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class APICharityScraper { + private final URL url; + private HttpURLConnection connection; + private final HttpClient client; + private final HttpRequest request; + + public APICharityScraper() throws IOException, URISyntaxException { + this.url = new URI("https://app.innsamlingskontrollen.no/api/public/v1/all").toURL(); + this.client = HttpClient.newHttpClient(); + this.connection = (HttpURLConnection) url.openConnection(); + this.request = HttpRequest.newBuilder() + .uri(url.toURI()) + .GET() + .build(); + } + + // Checks whether a connection returns a get response + public boolean checkConnection() throws IOException { + this.connection = (HttpURLConnection) url.openConnection(); + this.connection.setRequestMethod("GET"); + this.connection.connect(); + + int response = this.connection.getResponseCode(); + String response_text = this.connection.getResponseMessage(); + + if (response == 200) { + return true; + } + throw new RuntimeException("Connection failed : HTTP error code : " + response + ". \n Response text: " + + response_text); + } + + public String getJSONData() throws IOException, InterruptedException { + // Gets the JSON data and stores it in response body + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + return response.body(); + } + public List parseJSON(String JSONData) { + Gson gson = new Gson(); + APICharityData[] charityData = gson.fromJson(JSONData, APICharityData[].class); + + if (charityData == null) { + return new ArrayList<>(); + } + // Returns mutable list of JSON data converted to APICharityData Objects + return new ArrayList<>(Arrays.asList(charityData)); + } + +} From ebe15244c7d031e0eef080cb9d716bda0d349c25 Mon Sep 17 00:00:00 2001 From: Roar Date: Fri, 27 Feb 2026 10:24:35 +0100 Subject: [PATCH 013/123] Added APICharityScraperTest test class. Added testclass for the scraper with some test methods. --- .../team6/models/APICharityScraperTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityScraperTest.java diff --git a/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityScraperTest.java b/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityScraperTest.java new file mode 100644 index 0000000..39d7487 --- /dev/null +++ b/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityScraperTest.java @@ -0,0 +1,39 @@ +package ntnu.sytemutvikling.team6.models; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URISyntaxException; + +import static org.junit.jupiter.api.Assertions.*; + +class APICharityScraperTest { + @Test + void connectionToDataBaseShouldReturnTrue() throws IOException, URISyntaxException { + var con = new APICharityScraper(); + + assertTrue(con.checkConnection(), "Get response should be 200 (success)"); + } + + + @Test + void ParsedJSONShouldHaveCorrectValues() throws IOException, URISyntaxException { + var con = new APICharityScraper(); + String JSONData = "[{\"org_number\":\"938419264\",\"name\":\"Misjonsalliansen\",\"status\":\"approved\"," + + "\"url\":\"https:\\/\\/www.innsamlingskontrollen.no\\/organisasjoner\\/misjonsalliansen\\/\"," + + "\"is_pre_approved\":false}]"; + var list = con.parseJSON(JSONData); + + assertEquals("938419264", list.getFirst().getOrg_number(), + "Parsed organization number should be correct."); + assertEquals("Misjonsalliansen", list.getFirst().getName(), + "Parsed name should be correct"); + assertEquals("approved", list.getFirst().getStatus(), + "Parsed status should be correct."); + assertEquals("https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/", + list.getFirst().getUrl(), "Parsed url should be correct."); + assertFalse(list.getFirst().getIs_pre_approved(), "Parsed is_pre_approved boolean should " + + "be correct."); + } + +} \ No newline at end of file From f6cfff807393afc5e01d5a34d63bb5ab05be92ef Mon Sep 17 00:00:00 2001 From: Roar Date: Tue, 3 Mar 2026 12:06:15 +0100 Subject: [PATCH 014/123] Updated pom.xml. Added mockito as a dependency to make testing of http requests easier. --- helpmehelpapplication/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helpmehelpapplication/pom.xml b/helpmehelpapplication/pom.xml index 4a67e62..863b97a 100644 --- a/helpmehelpapplication/pom.xml +++ b/helpmehelpapplication/pom.xml @@ -40,6 +40,12 @@ gson 2.13.2 + + org.mockito + mockito-core + 5.21.0 + test + From 9ebb4762b0e9ff54da20f95773378dc931999ee0 Mon Sep 17 00:00:00 2001 From: Roar Date: Tue, 3 Mar 2026 12:10:40 +0100 Subject: [PATCH 015/123] Updated APICharityScraper. Replaced the connection/get request logic to make it easier to mocktest with mockito. --- .../team6/models/APICharityScraper.java | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java index 27f2e60..8e93637 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java @@ -1,7 +1,6 @@ package ntnu.sytemutvikling.team6.models; import com.google.gson.Gson; - import java.io.IOException; import java.net.*; import java.net.http.HttpClient; @@ -12,35 +11,28 @@ import java.util.List; public class APICharityScraper { - private final URL url; - private HttpURLConnection connection; private final HttpClient client; private final HttpRequest request; - public APICharityScraper() throws IOException, URISyntaxException { - this.url = new URI("https://app.innsamlingskontrollen.no/api/public/v1/all").toURL(); - this.client = HttpClient.newHttpClient(); - this.connection = (HttpURLConnection) url.openConnection(); + + public APICharityScraper(HttpClient client) throws URISyntaxException { + this.client = client; this.request = HttpRequest.newBuilder() - .uri(url.toURI()) + .uri(new URI("https://app.innsamlingskontrollen.no/api/public/v1/all")) .GET() .build(); } // Checks whether a connection returns a get response - public boolean checkConnection() throws IOException { - this.connection = (HttpURLConnection) url.openConnection(); - this.connection.setRequestMethod("GET"); - this.connection.connect(); - - int response = this.connection.getResponseCode(); - String response_text = this.connection.getResponseMessage(); + public boolean checkConnection() throws IOException, InterruptedException { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - if (response == 200) { + if (response.statusCode() == 200) { return true; } - throw new RuntimeException("Connection failed : HTTP error code : " + response + ". \n Response text: " + - response_text); + throw new RuntimeException("Connection failed: HTTP error code: " + response.statusCode() + + "\nResponse text: " + response.body() + ); } public String getJSONData() throws IOException, InterruptedException { @@ -49,6 +41,7 @@ public String getJSONData() throws IOException, InterruptedException { return response.body(); } + public List parseJSON(String JSONData) { Gson gson = new Gson(); APICharityData[] charityData = gson.fromJson(JSONData, APICharityData[].class); From 61520b04c0f4604f10f2ef81d6e516dcbdb15f3b Mon Sep 17 00:00:00 2001 From: Roar Date: Tue, 3 Mar 2026 12:13:13 +0100 Subject: [PATCH 016/123] Updated APICharityScraperTest. Changed test to use mocked http-responses using mockito for more reliable testing. --- .../team6/models/APICharityScraperTest.java | 90 +++++++++++++++---- 1 file changed, 72 insertions(+), 18 deletions(-) diff --git a/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityScraperTest.java b/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityScraperTest.java index 39d7487..515e616 100644 --- a/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityScraperTest.java +++ b/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityScraperTest.java @@ -1,39 +1,93 @@ package ntnu.sytemutvikling.team6.models; import org.junit.jupiter.api.Test; - import java.io.IOException; import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; + +import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; class APICharityScraperTest { @Test - void connectionToDataBaseShouldReturnTrue() throws IOException, URISyntaxException { - var con = new APICharityScraper(); + void checkConnectionShouldReturnTrueOn200ResponseCode() throws IOException, URISyntaxException, InterruptedException { + HttpClient mockClient = mock(HttpClient.class); + var mockResponse = mock(HttpResponse.class); + + when(mockResponse.statusCode()).thenReturn(200); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(mockResponse); + + var con = new APICharityScraper(mockClient); assertTrue(con.checkConnection(), "Get response should be 200 (success)"); } + @Test + void checkConnectionShouldThrowRuntimeExceptionIfConnectionIsNotASuccess() throws URISyntaxException, IOException, InterruptedException { + var mockClient = mock(HttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + + when(mockResponse.statusCode()).thenReturn(500); + when(mockResponse.body()).thenReturn("Internal server error."); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(mockResponse); + + var con = new APICharityScraper(mockClient); + + assertThrows(RuntimeException.class, con::checkConnection, + "The connection is not successful and should throw a RuntimeException."); + } @Test - void ParsedJSONShouldHaveCorrectValues() throws IOException, URISyntaxException { - var con = new APICharityScraper(); - String JSONData = "[{\"org_number\":\"938419264\",\"name\":\"Misjonsalliansen\",\"status\":\"approved\"," + + void getJSONDataShouldReturnCorrectJSONString() throws IOException, InterruptedException, URISyntaxException { + String response = "[{\"org_number\":\"938419264\",\"name\":\"Misjonsalliansen\",\"status\":\"approved\"," + "\"url\":\"https:\\/\\/www.innsamlingskontrollen.no\\/organisasjoner\\/misjonsalliansen\\/\"," + "\"is_pre_approved\":false}]"; - var list = con.parseJSON(JSONData); - - assertEquals("938419264", list.getFirst().getOrg_number(), - "Parsed organization number should be correct."); - assertEquals("Misjonsalliansen", list.getFirst().getName(), - "Parsed name should be correct"); - assertEquals("approved", list.getFirst().getStatus(), - "Parsed status should be correct."); - assertEquals("https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/", - list.getFirst().getUrl(), "Parsed url should be correct."); - assertFalse(list.getFirst().getIs_pre_approved(), "Parsed is_pre_approved boolean should " + - "be correct."); + + var mockClient = mock(HttpClient.class); + var mockResponse = mock(HttpResponse.class); + + when(mockResponse.body()).thenReturn(response); + when(mockClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(mockResponse); + + var con = new APICharityScraper(mockClient); + + assertEquals(response, con.getJSONData(), "The extracted JSON data should be the same " + + "as the data from the API."); + } + + + @Test + void parsedJSONShouldHaveCorrectValues() throws URISyntaxException { + APICharityScraper con = new APICharityScraper(HttpClient.newHttpClient()); + + String JSONData = "[{\"org_number\":\"938419264\",\"name\":\"Misjonsalliansen\",\"status\":\"approved\"," + + "\"url\":\"https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/\",\"" + + "is_pre_approved\":false}]"; + List list = con.parseJSON(JSONData); + + APICharityData d = list.getFirst(); + + assertEquals("938419264", d.getOrg_number(), "Org_number parameter " + + "should be correct."); + assertEquals("Misjonsalliansen", d.getName(), "Name parameter should be correct."); + assertEquals("approved", d.getStatus(), "Status parameter should be correct."); + assertEquals("https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/", d.getUrl(), + "Url parameter should be correct."); + assertFalse(d.getIs_pre_approved(), "Is_pre_approved parameter should be correct."); + } + + @Test + void parsedJSONOfNullShouldReturnEmptyList() throws URISyntaxException { + APICharityScraper con = new APICharityScraper(HttpClient.newHttpClient()); + + List list = con.parseJSON(null); + + assertTrue(list.isEmpty(), "List should be empty due to only parsing " + + "null objects."); } } \ No newline at end of file From 01929705b96c2f412373bc788e579720d04f9443 Mon Sep 17 00:00:00 2001 From: Roar Date: Tue, 3 Mar 2026 12:14:01 +0100 Subject: [PATCH 017/123] Added APICharityDataTest Added test for constructor/getters for ApiCharityData. --- .../team6/models/APICharityDataTest.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityDataTest.java diff --git a/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityDataTest.java b/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityDataTest.java new file mode 100644 index 0000000..c863686 --- /dev/null +++ b/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityDataTest.java @@ -0,0 +1,32 @@ +package ntnu.sytemutvikling.team6.models; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class APICharityDataTest { + + @Test + void initialParametersShouldBeCorrect() { + APICharityData charity = new APICharityData( + "938419264", "Misjonsalliansen", "approved", + "https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/", false + ); + + assertEquals("938419264", charity.getOrg_number(), + "Org_number parameter of APICharityData should be correct."); + assertEquals("Misjonsalliansen", charity.getName(), + "Name parameter of APICharityData should be correct."); + assertEquals("approved", charity.getStatus(), + "Status parameter of APICharityData should be correct."); + assertEquals("https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/", + charity.getUrl(), "Url parameter of APICharityData should be correct."); + assertFalse(charity.getIs_pre_approved(), "Is_pre_approved parameter of APICharityData should " + + "be correct."); + + } + + + + +} \ No newline at end of file From 8b170cf8c26d39a163777659e25a9aaaf8dc8166 Mon Sep 17 00:00:00 2001 From: Roar Date: Wed, 4 Mar 2026 20:05:08 +0100 Subject: [PATCH 018/123] Updated pom.xml. Added mysql-connector-j as a dependency. --- helpmehelpapplication/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helpmehelpapplication/pom.xml b/helpmehelpapplication/pom.xml index 863b97a..e921a97 100644 --- a/helpmehelpapplication/pom.xml +++ b/helpmehelpapplication/pom.xml @@ -46,6 +46,11 @@ 5.21.0 test + + com.mysql + mysql-connector-j + 9.6.0 + From c0efba400986c88570dfbb4b07416a79a0cd1987 Mon Sep 17 00:00:00 2001 From: Roar Date: Wed, 4 Mar 2026 20:08:07 +0100 Subject: [PATCH 019/123] Updated APICharityScraper. Added a streams filter to remove "obs" status values in the JSON data from the API, due to non-unique org_number values. Org_number is used as primary key, so this caused issues with database. --- .../sytemutvikling/team6/models/APICharityScraper.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java index 8e93637..8bb1b1d 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java @@ -49,8 +49,11 @@ public List parseJSON(String JSONData) { if (charityData == null) { return new ArrayList<>(); } - // Returns mutable list of JSON data converted to APICharityData Objects - return new ArrayList<>(Arrays.asList(charityData)); + + // Filters out "obs" status due to non-unique org_number values + return Arrays.stream(charityData) + .filter(c-> !"obs".equalsIgnoreCase(c.getStatus())) + .toList(); } } From 5c8c8a649f8de84b800773a52b5caf73cba0f3d3 Mon Sep 17 00:00:00 2001 From: Roar Date: Wed, 4 Mar 2026 20:19:44 +0100 Subject: [PATCH 020/123] Added DatabaseManager class. Working manager class for handling creation, insertion, and deletion of tables and records. Currently, supports creating charities table, updating the table with values from the IK API, and doing a parity check, removing values that doesn't exist in the IK API. --- .../team6/models/DatabaseManager.java | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java new file mode 100644 index 0000000..6d91982 --- /dev/null +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java @@ -0,0 +1,127 @@ +package ntnu.sytemutvikling.team6.models; + +import java.sql.*; +import java.util.List; + +public class DatabaseManager { + // Database environment is MySQL + private final String databaseURL; + private final String username; + private final String password; + + // Values stored in system environment variables for security (using ntnu's phpmyadmin for this project) + public DatabaseManager() { + this.databaseURL = System.getenv("HMH_DB_URL"); + this.username = System.getenv("HMH_DB_USERNAME"); + this.password = System.getenv("HMH_DB_PASSWORD"); + + if (this.databaseURL == null || this.databaseURL.isBlank()) { + throw new IllegalStateException("Database environment variable URL has not been set"); + } + + if (this.username == null || this.username.isBlank()) { + throw new IllegalStateException("Username environment variable has not been set"); + } + + if (this.password == null || this.password.isBlank()) { + throw new IllegalStateException("Password environment variable has not been set"); + } + } + + public void createCharitiesTable() { + // Creates table if it doesn't exist + String sql_create_main_table = """ + CREATE TABLE IF NOT EXISTS charities ( + org_number VARCHAR(100) PRIMARY KEY, + name VARCHAR(255), + status VARCHAR(50), + url VARCHAR(255), + is_pre_approved BOOLEAN + )"""; + + try (Connection conn = DriverManager.getConnection(databaseURL, username, password); + Statement s = conn.createStatement()) { + + s.execute(sql_create_main_table); + } catch (SQLException e) { + throw new RuntimeException("Error creating tables."); + } + } + + public void updateCharities(List charities) { + // Inserts values in APICharityData to table + + try (Connection conn = DriverManager.getConnection(databaseURL, username, password)) + { + // Inserts objects values into charities + String sql_update = """ + INSERT INTO charities (org_number, name, status, url, is_pre_approved) + VALUES (?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + name = VALUES(name), + status = VALUES(status), + url = VALUES(url), + is_pre_approved = VALUES(is_pre_approved) + """; + + PreparedStatement s = conn.prepareStatement(sql_update); + + for (APICharityData charity : charities) { + s.setString(1, charity.getOrg_number().replaceAll("\\s", "")); + s.setString(2, charity.getName()); + s.setString(3, charity.getStatus()); + s.setString(4, charity.getUrl()); + s.setBoolean(5, charity.getIs_pre_approved()); + + s.addBatch(); + } + s.executeBatch(); + + // Removed temp table used for parity check + String sql_drop_temp_table = "DROP TABLE IF EXISTS temp"; + PreparedStatement drop_temp_table = conn.prepareStatement(sql_drop_temp_table); + drop_temp_table.execute(); + + // Creates a temp table used for parity check + String sql_create_temp_table = "CREATE TABLE temp (org_number VARCHAR(100) PRIMARY KEY)"; + PreparedStatement temp_create = conn.prepareStatement(sql_create_temp_table); + temp_create.execute(); + + // Inserts org_number into the temp table + String sql_update_temp_table = "INSERT INTO temp (org_number) VALUES (?)"; + PreparedStatement temp_update = conn.prepareStatement(sql_update_temp_table); + for (APICharityData charity : charities) { + temp_update.setString(1, charity.getOrg_number().replaceAll("\\s", "")); + temp_update.addBatch(); + } + temp_update.executeBatch(); + + // Removes records from main charities table, that don't exist in the temp table. + // This makes it so if any organization is removed by IK, it will also be removed from the database. + String sql_delete_old_records = """ + DELETE FROM charities c + WHERE NOT EXISTS ( + SELECT 1 + FROM temp t + WHERE c.org_number = t.org_number + ) + """; + PreparedStatement delete_old_records = conn.prepareStatement(sql_delete_old_records); + delete_old_records.execute(); + + // Deletes temporary table + String sql_delete_temp_table = "DROP TABLE IF EXISTS temp"; + PreparedStatement delete_temp_table = conn.prepareStatement(sql_delete_temp_table); + delete_temp_table.execute(); + + + + + } catch (SQLException e) { + // prints whole sql error message + e.printStackTrace(); + throw new RuntimeException("ERROR: Something went wrong during updating charities table."); + } + + } +} \ No newline at end of file From 9cb3206d33d76bdde7eb8a95f613fac2c01abb97 Mon Sep 17 00:00:00 2001 From: Roar Date: Wed, 4 Mar 2026 20:45:46 +0100 Subject: [PATCH 021/123] Updated APICharityData Added javadoc descriptions, throws an exception if org_number is invalid, and automatically normalizes the value of org_number. --- .../team6/models/APICharityData.java | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityData.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityData.java index d94db88..10f3acf 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityData.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityData.java @@ -1,5 +1,17 @@ package ntnu.sytemutvikling.team6.models; +/** + * Represents data parsed from the IK API JSON response. + * Instances are immutable; to update any value, a new object must be created. + *

+ * Receives data directly from {@link APICharityScraper} or {@link IKOrganizationScraper}. + *

+ *

+ * {@code org_number} should be a unique number, as it is used as a primary key + * in {@link DatabaseManager}. + *

+ */ + public class APICharityData { private final String org_number; private final String name; @@ -7,30 +19,73 @@ public class APICharityData { private final String url; private final boolean is_pre_approved; + /** + * Constructs a new APICharityData object. + * @param org_number a unique number that identifies the organization + * @param name the name of the organization + * @param status {@code approved} for approved organizations, + * {@code obs} for non-approved organizations + * @param url the URL for more info about the organization on the IK domain + * @param is_pre_approved whether the organization was pre-approved + */ + public APICharityData(String org_number, String name, String status, String url, boolean is_pre_approved) { - this.org_number = org_number; + if (org_number == null || org_number.isBlank()) { + throw new IllegalArgumentException("ERROR: Org number cannot be null or blank"); + } + this.org_number = org_number.replaceAll("\\s", ""); this.name = name; this.status = status; this.url = url; this.is_pre_approved = is_pre_approved; } + /** + * Returns the organization number. Must not be {@code null} or blank. + * + * @return the organization number + */ + public String getOrg_number() { return this.org_number; } + /** + * Returns the name of the organization. Whitespace removed. + * + * @return the name of the organization + */ + public String getName() { return name; } + /** + * Returns whether the organization is approved or not + * @return the approved status of the organization + */ + public String getStatus() { return status; } + /** + * Returns the URL of the organizations information page on IK + * + * @return the URL for more info about the organization + */ + public String getUrl() { return url; } + /** + * Returns whether the organization was pre-approved. + * + * @return {@code true} if organization was pre-approved
+ * {@code false} otherwise + */ + public boolean getIs_pre_approved() { return this.is_pre_approved; } From a723cedda1b6f61891a73c4bb0f0d9b7fcc7ff11 Mon Sep 17 00:00:00 2001 From: Roar Date: Wed, 4 Mar 2026 21:01:39 +0100 Subject: [PATCH 022/123] Updated APICharityDataTest Added tests for new logic regarding org_number. --- .../team6/models/APICharityDataTest.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityDataTest.java b/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityDataTest.java index c863686..a015ac1 100644 --- a/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityDataTest.java +++ b/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityDataTest.java @@ -26,6 +26,39 @@ void initialParametersShouldBeCorrect() { } + @Test + void org_numberWhiteSpaceShouldBeremoved() { + String org_number = "938 4192 64"; + APICharityData charity = new APICharityData( + org_number, "Misjonsalliansen", "approved", + "https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/", false + ); + + assertEquals(org_number.replaceAll("\\s",""), charity.getOrg_number(), + "Org_number should not contain whitespace."); + } + + @Test + void nullOrg_numberShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> new APICharityData(null, "Misjonsalliansen", + "approved", "https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/", + false), "Null org_number should not be allowed."); + } + + @Test + void blankOrg_numberShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> new APICharityData(" ", "Misjonsalliansen", + "approved", "https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/", + false), "Blank org_number should not be allowed."); + } + + @Test + void emptyOrg_numberShouldThrowIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> new APICharityData("", "Misjonsalliansen", + "approved", "https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/", + false), "Null org_number should not be allowed."); + } + From 636351e20b725d5647844beaf38271e18740283e Mon Sep 17 00:00:00 2001 From: Roar Date: Wed, 4 Mar 2026 21:22:34 +0100 Subject: [PATCH 023/123] Updated APICharityScraper Added JavaDocs. --- .../team6/models/APICharityScraper.java | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java index 8bb1b1d..e92baca 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java @@ -10,10 +10,19 @@ import java.util.Arrays; import java.util.List; +/** + * Fetches JSON information from the IK API and parses the JSON into a list of {@link APICharityData} objects. + */ + public class APICharityScraper { private final HttpClient client; private final HttpRequest request; + /** + * Constructs a new APICharityScraper object. + * @param client the client responsible for making the http connection. + * @throws URISyntaxException if the API URL is malformed or violates URI syntax rules + */ public APICharityScraper(HttpClient client) throws URISyntaxException { this.client = client; @@ -23,7 +32,15 @@ public APICharityScraper(HttpClient client) throws URISyntaxException { .build(); } - // Checks whether a connection returns a get response + /** + * Checks if the http request returns an 'OK' response. + * + * @return {@code true} if connection was successful + * @throws IOException if an I/O error occurs while sending or receiving the HTTP reques + * @throws InterruptedException if the operation is interrupted while waiting for the response + * @throws RuntimeException if the connection is unsuccessful + */ + public boolean checkConnection() throws IOException, InterruptedException { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); @@ -35,13 +52,28 @@ public boolean checkConnection() throws IOException, InterruptedException { ); } + /** + * Fetches the JSON data from the IK API and stores it in a String. + * + * @return a String of the JSON values in IK API + * @throws IOException if an I/O error occurs while sending or receiving the HTTP request + * @throws InterruptedException if the operation is interrupted while waiting for the response + */ + public String getJSONData() throws IOException, InterruptedException { - // Gets the JSON data and stores it in response body HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); return response.body(); } + /** + * Parses the JSON data using gson and creates a list of APICharityData objects. + * + * @param JSONData the {@code String} of JSON data to be parsed + * @return a list of APICharityDaya objects + * @throws com.google.gson.JsonSyntaxException if the provided JSON is not valid + */ + public List parseJSON(String JSONData) { Gson gson = new Gson(); APICharityData[] charityData = gson.fromJson(JSONData, APICharityData[].class); From e99d6c72beb8207fbed0efa8793beb6f42f757fb Mon Sep 17 00:00:00 2001 From: Roar Date: Wed, 4 Mar 2026 21:23:39 +0100 Subject: [PATCH 024/123] Updated APICharityScraperTest Added a test to check that JSON entries with status:'obs' gets ignored. --- .../team6/models/APICharityScraperTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityScraperTest.java b/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityScraperTest.java index 515e616..e47431f 100644 --- a/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityScraperTest.java +++ b/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/APICharityScraperTest.java @@ -90,4 +90,18 @@ void parsedJSONOfNullShouldReturnEmptyList() throws URISyntaxException { "null objects."); } + @Test + void shouldRemoveObsStatusEntries() throws URISyntaxException { + APICharityScraper con = new APICharityScraper(HttpClient.newHttpClient()); + + String JSONData = "[{\"org_number\":\"938419264\",\"name\":\"Misjonsalliansen\",\"status\":\"obs\"," + + "\"url\":\"https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/\",\"" + + "is_pre_approved\":false}]"; + List list = con.parseJSON(JSONData); + + assertEquals(0, list.size(),"Entries containing 'obs' should be ignored, so list should " + + "be empty."); + + } + } \ No newline at end of file From c6ee1c9e4ffb6481d791d6b9993badfe7ef0b7ed Mon Sep 17 00:00:00 2001 From: Roar Date: Wed, 4 Mar 2026 22:43:07 +0100 Subject: [PATCH 025/123] Updated DatabaseManager Added JavaDocs and printStackTrace to createCharitiesTable method. --- .../team6/models/DatabaseManager.java | 59 ++++++++++++++++--- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java index 6d91982..a4e2ce8 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java @@ -3,12 +3,34 @@ import java.sql.*; import java.util.List; +/** + * Manages creation and synchronization of database tables in a MySQL database. + *

+ * This class is responsible for establishing connections using environment variables + * and maintaining the {@code charities} table based on data retrieved from the IK API. + *

+ */ + public class DatabaseManager { - // Database environment is MySQL private final String databaseURL; private final String username; private final String password; + /** + Constructs a new {@code DatabaseManager} using database credentials + * retrieved from system environment variables. + *

+ * Required environment variables: + *

    + *
  • {@code HMH_DB_URL}
  • + *
  • {@code HMH_DB_USERNAME}
  • + *
  • {@code HMH_DB_PASSWORD}
  • + *
+ *

+ * + * @throws IllegalStateException if either databaseURL, username, or password is {@code null} or blank + */ + // Values stored in system environment variables for security (using ntnu's phpmyadmin for this project) public DatabaseManager() { this.databaseURL = System.getenv("HMH_DB_URL"); @@ -28,8 +50,15 @@ public DatabaseManager() { } } + /** + * Creates the {@code charities} table if it does not already exist. + *

+ * The table structure is based on fields from {@link APICharityData}. + *

+ * @throws RuntimeException if a {@link SQLException} occurs while creating the table + */ + public void createCharitiesTable() { - // Creates table if it doesn't exist String sql_create_main_table = """ CREATE TABLE IF NOT EXISTS charities ( org_number VARCHAR(100) PRIMARY KEY, @@ -44,12 +73,28 @@ url VARCHAR(255), s.execute(sql_create_main_table); } catch (SQLException e) { + e.printStackTrace(); throw new RuntimeException("Error creating tables."); } } + /** + * Synchronizes the {@code charities} table with the provided list of APICharityData. + *

+ * Summary of function: + *

    + *
  • Inserts new records
  • + *
  • Updates existing records using {@code ON DUPLICATE KEY UPDATE}
  • + *
  • Removes database records that are not present in the provided list
  • + *
+ * A temporary table is used to determine which records should be deleted. + *

+ * + * @param charities a list of ApiCharityDaya objects + * @throws RuntimeException if a {@link SQLException} occurs during database synchronization + */ + public void updateCharities(List charities) { - // Inserts values in APICharityData to table try (Connection conn = DriverManager.getConnection(databaseURL, username, password)) { @@ -77,17 +122,15 @@ INSERT INTO charities (org_number, name, status, url, is_pre_approved) } s.executeBatch(); - // Removed temp table used for parity check + // Temporary table for parity check String sql_drop_temp_table = "DROP TABLE IF EXISTS temp"; PreparedStatement drop_temp_table = conn.prepareStatement(sql_drop_temp_table); drop_temp_table.execute(); - // Creates a temp table used for parity check String sql_create_temp_table = "CREATE TABLE temp (org_number VARCHAR(100) PRIMARY KEY)"; PreparedStatement temp_create = conn.prepareStatement(sql_create_temp_table); temp_create.execute(); - // Inserts org_number into the temp table String sql_update_temp_table = "INSERT INTO temp (org_number) VALUES (?)"; PreparedStatement temp_update = conn.prepareStatement(sql_update_temp_table); for (APICharityData charity : charities) { @@ -96,8 +139,7 @@ INSERT INTO charities (org_number, name, status, url, is_pre_approved) } temp_update.executeBatch(); - // Removes records from main charities table, that don't exist in the temp table. - // This makes it so if any organization is removed by IK, it will also be removed from the database. + // Removes records not found in the list from charities table String sql_delete_old_records = """ DELETE FROM charities c WHERE NOT EXISTS ( @@ -118,7 +160,6 @@ WHERE NOT EXISTS ( } catch (SQLException e) { - // prints whole sql error message e.printStackTrace(); throw new RuntimeException("ERROR: Something went wrong during updating charities table."); } From 273d9a51bc8093d86b8adc3179dbf8fd4a4da8de Mon Sep 17 00:00:00 2001 From: Roar Date: Thu, 5 Mar 2026 00:35:01 +0100 Subject: [PATCH 026/123] Updated DatabaseManager Made sql statements into transaction statements for increased stability. Converted "temp" table into a fully temporary table. Reused a single String variable for sql queries instead of one variable per statement. --- .../team6/models/DatabaseManager.java | 93 +++++++++++-------- 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java index a4e2ce8..eacb4e7 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java @@ -59,7 +59,7 @@ public DatabaseManager() { */ public void createCharitiesTable() { - String sql_create_main_table = """ + String sql_query = """ CREATE TABLE IF NOT EXISTS charities ( org_number VARCHAR(100) PRIMARY KEY, name VARCHAR(255), @@ -71,7 +71,7 @@ url VARCHAR(255), try (Connection conn = DriverManager.getConnection(databaseURL, username, password); Statement s = conn.createStatement()) { - s.execute(sql_create_main_table); + s.execute(sql_query); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException("Error creating tables."); @@ -95,11 +95,12 @@ url VARCHAR(255), */ public void updateCharities(List charities) { - - try (Connection conn = DriverManager.getConnection(databaseURL, username, password)) - { + Connection conn = null; + try { + conn = DriverManager.getConnection(databaseURL, username, password); + conn.setAutoCommit(false); // Inserts objects values into charities - String sql_update = """ + String sql_query = """ INSERT INTO charities (org_number, name, status, url, is_pre_approved) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE @@ -109,38 +110,39 @@ INSERT INTO charities (org_number, name, status, url, is_pre_approved) is_pre_approved = VALUES(is_pre_approved) """; - PreparedStatement s = conn.prepareStatement(sql_update); - - for (APICharityData charity : charities) { - s.setString(1, charity.getOrg_number().replaceAll("\\s", "")); - s.setString(2, charity.getName()); - s.setString(3, charity.getStatus()); - s.setString(4, charity.getUrl()); - s.setBoolean(5, charity.getIs_pre_approved()); - - s.addBatch(); + try (PreparedStatement s = conn.prepareStatement(sql_query)) { + for (APICharityData charity : charities) { + s.setString(1, charity.getOrg_number().replaceAll("\\s", "")); + s.setString(2, charity.getName()); + s.setString(3, charity.getStatus()); + s.setString(4, charity.getUrl()); + s.setBoolean(5, charity.getIs_pre_approved()); + + s.addBatch(); + } + s.executeBatch(); } - s.executeBatch(); - // Temporary table for parity check - String sql_drop_temp_table = "DROP TABLE IF EXISTS temp"; - PreparedStatement drop_temp_table = conn.prepareStatement(sql_drop_temp_table); - drop_temp_table.execute(); - String sql_create_temp_table = "CREATE TABLE temp (org_number VARCHAR(100) PRIMARY KEY)"; - PreparedStatement temp_create = conn.prepareStatement(sql_create_temp_table); - temp_create.execute(); + // Temporary table for parity check + sql_query = "CREATE TEMPORARY TABLE temp (org_number VARCHAR(100) PRIMARY KEY)"; + try (PreparedStatement temp_create = conn.prepareStatement(sql_query)) { + temp_create.execute(); + } String sql_update_temp_table = "INSERT INTO temp (org_number) VALUES (?)"; - PreparedStatement temp_update = conn.prepareStatement(sql_update_temp_table); - for (APICharityData charity : charities) { - temp_update.setString(1, charity.getOrg_number().replaceAll("\\s", "")); - temp_update.addBatch(); - } - temp_update.executeBatch(); + try (PreparedStatement temp_update = conn.prepareStatement(sql_update_temp_table)) { + for (APICharityData charity : charities) { + temp_update.setString(1, charity.getOrg_number().replaceAll("\\s", "")); + temp_update.addBatch(); + } + temp_update.executeBatch(); + } + + // Removes records not found in the list from charities table - String sql_delete_old_records = """ + sql_query = """ DELETE FROM charities c WHERE NOT EXISTS ( SELECT 1 @@ -148,20 +150,33 @@ WHERE NOT EXISTS ( WHERE c.org_number = t.org_number ) """; - PreparedStatement delete_old_records = conn.prepareStatement(sql_delete_old_records); - delete_old_records.execute(); - - // Deletes temporary table - String sql_delete_temp_table = "DROP TABLE IF EXISTS temp"; - PreparedStatement delete_temp_table = conn.prepareStatement(sql_delete_temp_table); - delete_temp_table.execute(); + try (PreparedStatement delete_old_records = conn.prepareStatement(sql_query)){ + delete_old_records.execute(); + } + conn.commit(); + } catch (SQLException e) { + if (conn != null) { + try { + conn.rollback(); + } catch (SQLException ex) { + ex.printStackTrace(); + } + } - } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException("ERROR: Something went wrong during updating charities table."); + } finally { + if (conn != null) { + try { + conn.setAutoCommit(true); + conn.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } } } From 3a453b34a54dd03eea95d77eaa7051ed37523451 Mon Sep 17 00:00:00 2001 From: Roar Date: Thu, 5 Mar 2026 09:21:08 +0100 Subject: [PATCH 027/123] Updated pom.xml Added H2 as a dependency, an in-memory mock database, useful for testing DatabaseManager logic. --- helpmehelpapplication/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helpmehelpapplication/pom.xml b/helpmehelpapplication/pom.xml index e921a97..ffac257 100644 --- a/helpmehelpapplication/pom.xml +++ b/helpmehelpapplication/pom.xml @@ -51,6 +51,12 @@ mysql-connector-j 9.6.0 + + com.h2database + h2 + 2.4.240 + test + From dce997e20cac360d4c5b8d199f486b1afbd828d2 Mon Sep 17 00:00:00 2001 From: Roar Date: Thu, 5 Mar 2026 09:28:50 +0100 Subject: [PATCH 028/123] Updated DatabaseManager Added a second constructor to make JUnit testing of the class feasible. Second constructor should not be used to create object in production code. --- .../team6/models/DatabaseManager.java | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java index eacb4e7..3d80458 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java @@ -50,6 +50,38 @@ public DatabaseManager() { } } + /** + * Constructs a new {@code DatabaseManager} using database credentials + *

+ * Used primarily for JUnit tests. Production should use the constructor + * using environment variables. + *

+ * + * @param databaseURL the url to the database + * @param username the username used to log in to the database + * @param password the password used to log in to the database + * @throws IllegalArgumentException if databaseURL, username, or password is + * {@code null} or blank + */ + + public DatabaseManager(String databaseURL, String username, String password) { + this.databaseURL = databaseURL; + this.username = username; + this.password = password; + + if (this.databaseURL == null || this.databaseURL.isBlank()) { + throw new IllegalArgumentException("Database environment variable URL has not been set"); + } + + if (this.username == null || this.username.isBlank()) { + throw new IllegalArgumentException("Username environment variable has not been set"); + } + + if (this.password == null || this.password.isBlank()) { + throw new IllegalArgumentException("Password environment variable has not been set"); + } + } + /** * Creates the {@code charities} table if it does not already exist. *

@@ -74,7 +106,7 @@ url VARCHAR(255), s.execute(sql_query); } catch (SQLException e) { e.printStackTrace(); - throw new RuntimeException("Error creating tables."); + throw new RuntimeException("Error creating table."); } } @@ -125,7 +157,7 @@ INSERT INTO charities (org_number, name, status, url, is_pre_approved) // Temporary table for parity check - sql_query = "CREATE TEMPORARY TABLE temp (org_number VARCHAR(100) PRIMARY KEY)"; + sql_query = "CREATE TEMPORARY TABLE IF NOT EXISTS temp (org_number VARCHAR(100) PRIMARY KEY)"; try (PreparedStatement temp_create = conn.prepareStatement(sql_query)) { temp_create.execute(); } From cf33cfa8958ddfd8cf4dbae4640b449c108b83cb Mon Sep 17 00:00:00 2001 From: Roar Date: Thu, 5 Mar 2026 10:37:27 +0100 Subject: [PATCH 029/123] Added DatabaseManagerTest Added some coverage tests to check if it functions as intended. Does not currently have 100% coverage. --- .../team6/models/DatabaseManagerTest.java | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/DatabaseManagerTest.java diff --git a/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/DatabaseManagerTest.java b/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/DatabaseManagerTest.java new file mode 100644 index 0000000..c73a57b --- /dev/null +++ b/helpmehelpapplication/src/test/java/ntnu/sytemutvikling/team6/models/DatabaseManagerTest.java @@ -0,0 +1,235 @@ +package ntnu.sytemutvikling.team6.models; + +import org.junit.jupiter.api.*; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import org.h2.jdbc.JdbcSQLNonTransientException; +import static org.junit.jupiter.api.Assertions.*; + +class DatabaseManagerTest { + + private DatabaseManager dbManager; + private String db_url; + private String username; + private String password; + + @BeforeEach + void setUp() throws SQLException{ + db_url = "jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1"; + username = "user"; + password = "123"; + dbManager = new DatabaseManager(db_url, username, password); + + // Removes tables due to quirk with H2 that keeps temporary tables + try (Connection conn = DriverManager.getConnection(db_url, username, password)) { + PreparedStatement endstmt = conn.prepareStatement("DROP TABLE IF EXISTS charities, temp"); + endstmt.execute(); + } + } + + @Test + void constructorShouldThrowIllegalArgumentExceptionIfDatabaseURLIsNull() { + assertThrows(IllegalArgumentException.class, () -> new DatabaseManager(null, username, password), + "DatabaseURL should not be allowed to be null."); + } + + @Test + void constructorShouldThrowIllegalArgumentExceptionIfDatabaseURLIsBlankOrEmpty() { + assertThrows(IllegalArgumentException.class, () -> new DatabaseManager(" ", username, password), + "DatabaseURL should not be allowed to be blank."); + assertThrows(IllegalArgumentException.class, () -> new DatabaseManager("", username, password), + "DatabaseURL should not be allowed to be empty."); + } + + @Test + void constructorShouldThrowIllegalArgumentExceptionIfUsernameIsNull() { + assertThrows(IllegalArgumentException.class, () -> new DatabaseManager(db_url, null, password), + "Database username should not be allowed to be null."); + } + + @Test + void constructorShouldThrowIllegalArgumentExceptionIfUsernameIsBlankOrEmpty() { + assertThrows(IllegalArgumentException.class, () -> new DatabaseManager(db_url, " ", password), + "Username should not be allowed to be blank."); + assertThrows(IllegalArgumentException.class, () -> new DatabaseManager(db_url, "", password), + "Username should not be allowed to be empty."); + } + + @Test + void constructorShouldThrowIllegalArgumentExceptionIfPasswordIsNull() { + assertThrows(IllegalArgumentException.class, () -> new DatabaseManager(db_url, username, null), + "Database password should not be allowed to be null."); + } + + @Test + void constructorShouldThrowIllegalArgumentExceptionIfPasswordIsBlankOrEmpty() { + assertThrows(IllegalArgumentException.class, () -> new DatabaseManager(db_url, username, " "), + "Password should not be allowed to be blank."); + assertThrows(IllegalArgumentException.class, () -> new DatabaseManager(db_url, username, ""), + "Password should not be allowed to be empty."); + } + + @Test + void createCharitiesTableShouldCreateTableSuccessfully() throws SQLException { + dbManager.createCharitiesTable(); + + try (Connection conn = DriverManager.getConnection(db_url, username, password)) { + ResultSet rs = conn.getMetaData().getTables(null, null, + "CHARITIES", null); + + assertTrue(rs.next(), "Connection to the database should be successful."); + } + } + + @Test + void updateCharitiesShouldInsertCorrectData() throws SQLException { + String org_number = "12345"; + String name = "Svindelorg"; + String status = "approved"; + String url = "https://www.svindel.no"; + boolean is_pre_approved = false; + + var charity = new APICharityData( + org_number, + name, + status, + url, + is_pre_approved + ); + + dbManager.createCharitiesTable(); + dbManager.updateCharities(List.of(charity)); + + try (Connection conn = DriverManager.getConnection(db_url, username, password); + PreparedStatement stmt = conn.prepareStatement("SELECT * FROM charities WHERE org_number = ?")) { + + stmt.setString(1, org_number); + ResultSet rs = stmt.executeQuery(); + + assertTrue(rs.next()); + assertEquals(org_number, rs.getString("org_number"), "Organization number should " + + "be correct."); + assertEquals(name, rs.getString("name"), "Name of the organization should be correct."); + assertEquals(status, rs.getString("status"), "Status of the organization should be " + + "correct."); + assertEquals(url, rs.getString("url"), "Url of the organization should be correct."); + assertEquals(is_pre_approved, rs.getBoolean("is_pre_approved"), "Is_pre_approved " + + "value should be correct."); + } + } + + @Test + void updateCharitiesShouldRemoveDataNotInList() throws SQLException { + String org_number = "12345"; + String name = "Svindelorg"; + String status = "approved"; + String url = "https://www.svindel.no"; + boolean is_pre_approved = false; + + var charity1 = new APICharityData( + org_number, + name, + status, + url, + is_pre_approved + ); + + org_number = "23456"; + name = "SvindelKoin"; + status = "approved"; + url = "https://www.svindel.net"; + is_pre_approved = true; + + var charity2 = new APICharityData( + org_number, + name, + status, + url, + is_pre_approved + ); + + org_number = "345672"; + name = "Arme Svindlere"; + status = "approved"; + url = "https://www.armesvindlere.com"; + is_pre_approved = false; + + var charity3 = new APICharityData( + org_number, + name, + status, + url, + is_pre_approved + ); + + var charityListBefore = new ArrayList(); + charityListBefore.add(charity1); + charityListBefore.add(charity2); + charityListBefore.add(charity3); + + var charityListAfter = new ArrayList(); + charityListAfter.add(charity1); + charityListAfter.add(charity3); + + dbManager.createCharitiesTable(); + dbManager.updateCharities(charityListBefore); + + try (Connection conn = DriverManager.getConnection(db_url, username, password)) { + PreparedStatement stmt = conn.prepareStatement("SELECT COUNT(org_number) AS number_b FROM charities"); + + ResultSet rs = stmt.executeQuery(); + + assertTrue(rs.next(), "Charities count row should exist."); + assertEquals(3, rs.getInt("number_b"), "The amount of org_numbers in the table" + + "should be 3."); + PreparedStatement endstmt = conn.prepareStatement("DROP TABLE IF EXISTS temp"); + endstmt.execute(); + } + + dbManager.updateCharities(charityListAfter); + + try (Connection conn = DriverManager.getConnection(db_url, username, password)) { + PreparedStatement stmt = conn.prepareStatement("SELECT COUNT(org_number) AS number_a FROM charities"); + + ResultSet rs = stmt.executeQuery(); + + assertTrue(rs.next(), "Charities count row should exist."); + assertEquals(2, rs.getInt("number_a"), "The amount of org_numbers in the table" + + "should be 2 due to removal of 1 table."); + } + } + + @Test + void tempTableShouldNotExistAfterUpdating() throws SQLException{ + try (Connection conn = DriverManager.getConnection(db_url, username, password)) { + PreparedStatement endstmt = conn.prepareStatement("DROP TABLE IF EXISTS charities, temp"); + endstmt.execute(); + } + String org_number = "12345"; + String name = "Svindelorg"; + String status = "approved"; + String url = "https://www.svindel.no"; + boolean is_pre_approved = false; + + var charity = new APICharityData( + org_number, + name, + status, + url, + is_pre_approved + ); + + dbManager.createCharitiesTable(); + dbManager.updateCharities(List.of(charity)); + + try (Connection conn = DriverManager.getConnection(db_url, username, password)) { + PreparedStatement stmt = conn.prepareStatement("SELECT * FROM temp"); + + ResultSet rs = stmt.executeQuery(); + + assertThrows(JdbcSQLNonTransientException.class, () -> rs.getString("org_number"), + "Exception about table being empty should be thrown."); + } + } +} \ No newline at end of file From cf0dde899584afbaaf34491e330a9f81302be0e9 Mon Sep 17 00:00:00 2001 From: Roar Date: Thu, 5 Mar 2026 10:38:26 +0100 Subject: [PATCH 030/123] Updated DatabaseManager Removed IF NOT EXISTS for temporary table as it gave unintended results in unit tests. --- .../java/ntnu/sytemutvikling/team6/models/DatabaseManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java index 3d80458..eb1530b 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java @@ -157,7 +157,7 @@ INSERT INTO charities (org_number, name, status, url, is_pre_approved) // Temporary table for parity check - sql_query = "CREATE TEMPORARY TABLE IF NOT EXISTS temp (org_number VARCHAR(100) PRIMARY KEY)"; + sql_query = "CREATE TEMPORARY TABLE temp (org_number VARCHAR(100) PRIMARY KEY)"; try (PreparedStatement temp_create = conn.prepareStatement(sql_query)) { temp_create.execute(); } From 04558b2c24457c0956b61c16ede12f159b5353ec Mon Sep 17 00:00:00 2001 From: AdrianBalunan Date: Thu, 5 Mar 2026 13:13:55 +0100 Subject: [PATCH 031/123] Feat: Gitignore target directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f28f5be..660d9b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # Adrian .vscode/ .idea/ +helpmehelpapplication/target From 22f5f24d83c8236a1fd76df8b4d75aa72e670783 Mon Sep 17 00:00:00 2001 From: AdrianBalunan Date: Thu, 5 Mar 2026 14:09:34 +0100 Subject: [PATCH 032/123] Feat: Dedicated Directory --- .../team6/models/{ => API}/APICharityData.java | 2 +- .../team6/models/{ => API}/APICharityScraper.java | 2 +- .../team6/models/{ => API}/DatabaseManager.java | 6 +++--- .../team6/models/{ => API}/IKOrganizationScraper.java | 3 ++- 4 files changed, 7 insertions(+), 6 deletions(-) rename helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/{ => API}/APICharityData.java (98%) rename helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/{ => API}/APICharityScraper.java (98%) rename helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/{ => API}/DatabaseManager.java (99%) rename helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/{ => API}/IKOrganizationScraper.java (97%) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityData.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/API/APICharityData.java similarity index 98% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityData.java rename to helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/API/APICharityData.java index 10f3acf..b58f60b 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityData.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/API/APICharityData.java @@ -1,4 +1,4 @@ -package ntnu.sytemutvikling.team6.models; +package ntnu.sytemutvikling.team6.models.API; /** * Represents data parsed from the IK API JSON response. diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/API/APICharityScraper.java similarity index 98% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java rename to helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/API/APICharityScraper.java index e92baca..e674505 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/APICharityScraper.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/API/APICharityScraper.java @@ -1,4 +1,4 @@ -package ntnu.sytemutvikling.team6.models; +package ntnu.sytemutvikling.team6.models.API; import com.google.gson.Gson; import java.io.IOException; diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/API/DatabaseManager.java similarity index 99% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java rename to helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/API/DatabaseManager.java index eb1530b..01b3223 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DatabaseManager.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/API/DatabaseManager.java @@ -1,4 +1,4 @@ -package ntnu.sytemutvikling.team6.models; +package ntnu.sytemutvikling.team6.models.API; import java.sql.*; import java.util.List; @@ -151,9 +151,9 @@ INSERT INTO charities (org_number, name, status, url, is_pre_approved) s.setBoolean(5, charity.getIs_pre_approved()); s.addBatch(); - } + } s.executeBatch(); - } + } // Temporary table for parity check diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/API/IKOrganizationScraper.java similarity index 97% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java rename to helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/API/IKOrganizationScraper.java index e43eef0..16dcc06 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/IKOrganizationScraper.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/API/IKOrganizationScraper.java @@ -1,4 +1,4 @@ -package ntnu.sytemutvikling.team6.models; +package ntnu.sytemutvikling.team6.models.API; import java.io.File; import java.io.FileWriter; @@ -8,6 +8,7 @@ import java.util.Collections; import java.util.List; import com.opencsv.CSVWriter; +import ntnu.sytemutvikling.team6.models.Organization; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; From 0d9ba9a339a155a78a8164044883ac530caec00a Mon Sep 17 00:00:00 2001 From: Robin Strand Prestmo Date: Tue, 10 Mar 2026 13:43:00 +0100 Subject: [PATCH 033/123] Added donationPage, and frontPage fxml files --- .../src/main/resources/fxml/donationPage.fxml | 247 +++++++++++ .../src/main/resources/fxml/frontPage.fxml | 383 ++++++++++++++++++ .../src/main/resources/images/Logo.png | Bin 0 -> 94987 bytes .../src/main/resources/images/img.png | Bin 0 -> 886 bytes 4 files changed, 630 insertions(+) create mode 100644 helpmehelpapplication/src/main/resources/fxml/donationPage.fxml create mode 100644 helpmehelpapplication/src/main/resources/fxml/frontPage.fxml create mode 100644 helpmehelpapplication/src/main/resources/images/Logo.png create mode 100644 helpmehelpapplication/src/main/resources/images/img.png diff --git a/helpmehelpapplication/src/main/resources/fxml/donationPage.fxml b/helpmehelpapplication/src/main/resources/fxml/donationPage.fxml new file mode 100644 index 0000000..d5f8881 --- /dev/null +++ b/helpmehelpapplication/src/main/resources/fxml/donationPage.fxml @@ -0,0 +1,247 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/helpmehelpapplication/src/main/resources/fxml/frontPage.fxml b/helpmehelpapplication/src/main/resources/fxml/frontPage.fxml new file mode 100644 index 0000000..079993c --- /dev/null +++ b/helpmehelpapplication/src/main/resources/fxml/frontPage.fxml @@ -0,0 +1,383 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/helpmehelpapplication/src/main/resources/images/Logo.png b/helpmehelpapplication/src/main/resources/images/Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ccddd514f94ffd9db7b887877c390d27086997af GIT binary patch literal 94987 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�FbqMNTCb>ZmMc`+rAwU6a7-9o< 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

LF@GwSB&WXl77hyvuoN>3wd%-1ZX#_ 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%0D+oEDy0h(7Vf z=4{|`;^FXl&eZt?ULH0X$sI8%ycsF_OPwj_`eG;~lmufPUdI^4dgqrbZDpCs&0{ZllU?|d?Z_|uM&z8SToxgdrJZB42oW*^&5ArMaCN?aapLqp3fC1aEW}`uTp?7ToI`bN zs8ceiYC*fk#>5RjcS4=e2fMVfAjX5%R$VEGuJHr{02ORzgFX|6#-BgIKB-UAz$qa1 zQ<8R80@$SCQJWIpp^C@n`yP}T^Cp)X98&SryOf}01)e-vjPRS}F@E{fQO$r!>+-JN zC`tFm8#DxQG7%qS)ThohOOv%I1A>O&g7xkc)9CyN(YLS&Ydj)I zxPU6~FG7dkJ%84TcaXwV1BQET%%}DSwZZ#A|7}C}G@BHnqfE^?X`ax@HNV#8!TGX@ z=J(yIS-L3Hig*G~&2?0vd`i zp3RZ{*q+>pDki|TEAa7N^ITu8r&dJ(w;|RsZm~9y-`V?M022F1*6pUUUwFC-Uyfxz zZN8Jp8s>MyCw2QX2olUl0fxz5u)rWSkBUd=T6{WnlwZPSKJO#odGG-MxI_!Pr4Zm9 zkkcvgS5pGWzV^Per_QbQvsc8yinIaMO z;4f?CbAsQ~tEMCT)@O%@9e-!jQ~gTr==R3qovRv)Pc<|(%_V;G-oamWLt*Nzm=68? z^OGrqVS>B{7A`|I$7QSn{)6t3Ee@=&wm@7;kklPk=yl=(@(vV6k~Ool~=gC z2PDHkq?{n^5Y(!$6Z_W&VWl`N;h8bw@j+)Hdu5dcJ;^blJ3JoHI{07F!?II$&OX?;QlwPhmb4NI8ouDYb_%{4-dAC~Vx)B-1UIU3 zwOXpDf+HdEDve8a=X7g&Nqv&%iMx=VRR zJ3i-B^qs=hlY5!O?nXzWHUL}vd95%Lt=wed@8qpr@%UpEoJKdw-T^nIAqmXJ`SHff zwjgBtmD|PHo$1`VxEMjvecv1u^B{R0r`QL5U%}4O;=#CAwZ|$k`Vwt=`$z_V1isvO z?+$H{)z+6j&3XQ3JHakFBdB-UKdp*?(-^N1^IU0;MrV-$p$6C3A)&6a3L6f}qYe*A6JErY& z_kYA-Y`6S?(Qi0#A~IpT$5WM@WpT`r_P9up27)^06Q@4Ez5Zm7-62t+SnlZV>JT?e z2WRNcC9m)!DFYh&+hp9M#oEXas+F{Ou`kHD^PV#*pMt_1L#pcz#%2r6`!Y0;hl@|M zWS;Vxs;$)>bhl?nb*u7K?Go-gOseKjmLk;?sWmESdarArm|FUCKzJPD_jvUhmF?yi zA~um;RnVYv`ST1b05cq_ii721x;5M3*ZE;IIka%#AU3 zXRC&uV6vt6Ns&C!_f-x3&>?1U;eeq7;i@jA7n}R_+An`U|9S@087)X@CQ8^6sqg_af0b7JfXf9Uamh8Ea53BPAE7(Wt#swo+~)<$j}iR%!v>Vd*cnw5 zSWit7a5!{F!x`BWiAw(Z~8qd zxaIe9V;HFeh5Si-$Jtipev+ILu4gb}79MhbO4p{{2`w$26Ji4OZH2YwoyTX>ut#1} z-FwbrZQ1H=;%?z+_Hic%Rc3&8LGkX6*i;Wv=n2(=Rc`HK-xTyh^_Qx71NMW|_mfQu z#TT+4DV92Ag)a0D1|dX=l8HkJ%LxJHiA0&Gv+Fg9~H+Y z)vXHlYRpHeQMofhQdrtjP{9#5+zMN0XQf)`NZGB^zHTRey6e&vSF+bSv_|cKAKO`nuon ziuv|{5K}J1RW+FZr6_&pJc3OA{z661phlhj}PH_CalZ1-)9r}odY6fxs9A7Lpg0NJskdh`QXge7Tt;YkaP4|Yinfvs$;JX#UGO9 z;&3mLBaInQjiT!PvSr;S=mnAzo8v89V`QI>X7gl};C;O@94|h#dTx2ed%aBd8}NfX zl6?fIG3N9t*4SRuNSJ?F`n1@Nky_z@^%*M*u;u1sSis{xJsLVM-od!p&9~$O@gIc(Q_7eOz#tIOnjvmD6U$dSanC;A2e%^Q4v{&@UpJdWMnQ!u2r zF4QYr5jX4Hp`)V<9Ix4HQTsJ%@v(f+p-!nOXq@L9hFUm}LvsptXLsWqPDpN8V+zIa zzXQ8Qg^n#$x-&Yo=WaJ=AId)@bMD17`wbI0C%OFn`8@b_K=5@Z`yv_KnRBAcjwQ%} z6f6Yi-s};!-uc46mesa1v`!4eN=Gr0mdr{2#5Y&GrB(U(Vc*(F1x~-)>*Jgwx6S+x zrs&$0$imC_aF6};&B}{+m9iT=exuT*JEqtvahgWo-_Zw5UlUg;u908%_KU&;>PTX3 zpFDERE%NT=x;rh9b+u8*`goz6{o+kajWPH5XWCwbi`_EnfARJSM;{}DdXwQ{gpKej zE+AWsZGulImenv(qdEtvYJ*%bPeH1e{Ia2*U-YA577?G%Iw362Pb{yYwe+F>oX<+_ zESOk)mkvo;TW(dpUGo;}4sjV9 zV+TZyrYIH8W}Vf?tl8*x;XBa%uBnDs!SqsHI33~|dk0)x-jyT9#k zk_r@~ht_I!e=m`6v0(PW)~7(`U9+4lNB1YvXnLU1|1E-QPTLaD%t1aKnzA?`kpgmf3kW>xFF zzPvZJIdB8OZsVVK3K4cxqliT40%AJhJecZ;pmlUOIN*$(jjwHu9q1PJmeEN(ZJ%_> zx(V@=eWx&F)Ss>WbdfwFxS0)k%0Ax%3+A4d?Z8fxVe)XswOt&h71|f63|ck)1FQf-1rIq(npfZ? zM_kD*N-)}}WiC#S`TvsdwBydx?4H}a-EbiC9v+?T)d$}Df9cY83--An*pP#CKAo=_ zp3!XY=x^v z>(}-?ce6NaxiueVKp z{h(vmrH=N(l|x^lL!sDtx~hv_@Y#jc3qn9jMymoy$9QPW3iruPF@?_DoVEH0xk&N* zJuuZv-kM;O{}EX;Qtoq#T<0y1_f4RVN70(E>OqB&qw>lyFgky|d^726;gy=Zf4PqH z=gXgpd*E@*Za(-60cSRKR2qRO>X_Ten7G<-mmBv-rYB2`$Z_E+?xu@qk40b4mB-)M z^4I&7a32z+v})gw`Skho5Der`#+h=}yCj4wyC-a}Y)%Y zlc%QGZJ~8^+!IzQ8c<;hKzUL9`wKVRy1OUfginNxEw8z4|CD`k*7?i`0s7hj`px1j zt&(SOVsOHw<9~6y@3wTkeQv-0C2wG&W-X_)ue2eqs)6@dJxgMgoD{a#3k1P;myO!8 z+HG2!u7AYb4(+Dhd~c#H56Hgz2dnqLlhd4=yujEitdV1O&n$1E22g<~pbC7IF*lDQ zs(xf;Y1=f2Yd{7)g>$%Ei~HwXe}(55 zLhkUm__UTmn#JYPfY=M|MpfQ^{6k%DQ_$}L<9T@>gfGfoL@W_4i#!uzcIVJiZ|nZf?U&WAGyD z_eyl%S(8Q6K5OY$*m@sOS?Q!7J$CJ0a3Pz=(20|%OB5-8El)4YBFnw`E2quKr?igL z@7?_Nm{7u`3R|k)S6jSE*Y2Bn&MpJf7U-}D6knR+?bv;CgtYPRN8S#c-_tsf-r=j+ zUfiFQB6$%O-5)+P0>2P%2Cc<;NLA4XajNKvZ~r4aA2z4gKlBOgHK3#wgk-Bq zf{H<#{CG$~nBw~6jO;g!y7v3WC-WQ7lh4*MrDhQA=+1u#ff!_T2Oo}*n^%)#ViwuG z)ApseK6mzH3p~?7E-w_{p7l8VQ(9#J9pAkH6x+`abT!#HGt1(^lQEbeww4Z2gXFn! zpH^Q-jrC1C_hLj&U;CkZbg`T>uLSw%yxm^d9&)h__+6ju-79h>ZTFr*1_i7~ils`? zX^wrwRxd=>60sHVq)*(WV{Ky?Rcl3(D2pkCsrc?TUh7tQKKvfTw+Z;l3iLRsoj^e( zmz#Xw3TYy<1N!xo>6>LNx~;2?-{GY@q-AXn&Uf}Q=m|>|R*#ajnHlx@pI%GP(5u;IDsVQ=Aldp}P#WEGN3Y+@ zWA^>@pdba9T}?i$ZMd4{_InFq)*i*RD3Up3zd0yjs96eYuE;ia7QP@OEsbz~wu9;& zQ33*!PePfS4m?XbiidvA5%0Z$|0HU;+trVyq?v`-0s1ZSfCQn{x&4HlK#Ca=kMfJ} z#3mHSNU~@?A1Oju6`Y**`*8<35bBDl7v2KcoF zh_~<5CA)k2p3@p4lU*UdsJWx2uO<0553vd zpPKd`lT3Y}QN9PljsIo$emz8N(`#44&e`NobY!~G%WzObiK=Hqw*VDK6qEY7|R~d#6FFuky4j^S8!FOI5$matUapGr+=w>zZjssTSSdd`C_ZoKgh zKDe?`ir|V#^}w0y=h>k6h9qR;#T{9GfpX4VG*-Kj=5NyV2&%!sPhr*0h7_G>dwJcj$!-R%DZEYD zseG~6ZT5;0vaK(OgHvUqC(tJ#QwbhqMHQ}B}A;E zs+_agGRo+}|7gx+e<2#W(pYY)oBR^?%SO)~dQpzLfX$j{8%!mx$O8BwwFcz9=K{UcbhR<^*v@Ki~K$f;n9jZvc}FIV`ox zb-q&Mo`+4>n3NP|X=mx8zrZF_CjKg+>}U;iF_kf%5p<;R7xhc6a^b8Wa_vYoy_En2bpfDNA5j|C3?{)7Y}&120?tiaDZ9$iTjXEnjQqH0d!ZwUf2J zJN;bUJ5M*}-OsjZ8G_`a;Laz44|=keiZ1>B6t2j*qK$1CSq5voIyU->zf!?+K&hAv zCCZ3kU`8qh{|HlA?bK#Z&=@!~3$_rpMwPrq3y0ZBEbxRy9yH|s! zh@p|K5UIDYJCYgATyG<0i@z?8#edENbhd(El3R55x$@B$4?AUfk6D8BK4zA^mSHcl zzkq@rJ`~VFTV#)~?0s5C8-GAwoieF26584ugZ}$$D|L;dv!X0N!o5B~8DW6uP~gW? zTSx&Fsd6}ge|22?t1zhc?e>>~Sm~7k zzK^v3-}xP3k%}f(-=^r0)IDZ|m}qP>MUG=puN8^ZAKBLs8xXLvm?jvv4c*)hsq4_v zmfltMnXGXT%Vi2G&cQejX&atB5g^dViZdF~BFk;%&oF)(ZIgo`H+fz_YI6CCc0~Q2 zYSp%mm#JCBd@-Q8Z1P07@gNg#gTbAYF4lGPG?6cnU5z+8K9ab2oW_*?Nc8HnGWGlT zdt%@WyKrCLL-DmpYER^AhVR9chy7UZy$TOUBB$C;r)2&(cR>cd?^fC%y0& zUk{rVD)3%zn^RXHEmE{$q7)_Da4}{GI?>yMr;o@hBK0Wp{T#ua4x+iO<^TPA;OA@u zW+bLcX#CVM%S!Rr;iXw>>H^}Dsup~*AhBDI9&1g1R^-`E&dr%@Vd=0tCY%9T;<*Ru zLGz@laN;2+DCrte6mdmB1iPd)L|pCbu|}XSspM=jsUpaACWvD;aCBeV=Q(s^vw{t- z827)L{2&wyFyOD6#A~XGNPX+;cQn9#VBPU~5`QQi5hm_AnstXO1et1}_d7y`pq zOXs_xyNirU;38pN`)1fZQyqD-(%XhlB*cMc-u+7!>Rrt z{Or4`dL+4g?;S+IRDdxE;oI4mHJpd0X3|fbPHo18YLn*D7DVnZ^`}(IcmE?Hs2FFLqvi_GFhdfPw5gx5m+_nqivOy2`4E#uslwr zJ7ZTPk5-S2M_|w|`|gK?gE66(WG}gkCRA*Oqyo6ybujQ7leqNTMH|m}d_v+_lSytP z%-aL9eRC9O+0E2=F6fGcj=ql71)#eOX$GSG_xQwaop5UV=ZTtx;g8j^p40DiLwp~A zVVr#*_Nj)w`al^9n`)>>gdghmzDSX|_g;`T6ccF4@jwyHFT{J!JN0hsDou==0)XcZ zBC$9`EAc~H^S~hYHfBG;!eOwM)>1NOGI1c&WYufO!j*Qp*pA^hr;LMD`5iYD(c^nF zc&>h)wPf;8j}TU|3F{X7l;>_elWpI#)PKr`~L`U3BGe_;p^a>#D`Kb;WEz^ z6RkBCQpJVF!+KOVw0021t}Uc**kZebmJImP5~LQNH(|~|-O2U_DvA`f%G6Z+3%63J zlQ6AlTu6q`x58jsMAcPx`?E)3vRm5^H`4d0D>V@F<~6m%#sB{sW&}>KuD=~dcl#7S+;v97u@x9GS*})I$?@o)ALBD`VZ^9b@*s2`_xjMtP#?k6ONY2j8Zny_h9Kk#9b``~2%aZPBqSRd*#&<1la4 zq(Zrx^&U{DMdNgA-{W=vN5giJY z{#z98_D!B>vwI0nJNHxjciQGsYzK*|W2)nxr*?6=<2LlP+pK?pVdF1Rr6TS!kB-Zw zkatnHM6l%lc97(#DK<1-MAI|JRiBGau=hV(qIL1GYYryFEN?Y8qdm^_`5)VdArj-b zBWUGAw1_4N3jK`xt=$dIZ5or-h^-p4+|40(zmoo2^=LB{YQL_o#U!CY)$lEIl3weEN`@R=`>Bjlg;8m>7J13aSg9X ziTDgt$-_nyn4Xu!S16q{&Ox?8tqolOZ({fO8?qgaWaW5qsC z3!v-DsPcpzrS}On9wt`QI!A^NXaLjJ`u?(fn-jhWkuTJuzqOm#Iv4^<2Xbv@b0-Ra z^p0J6*G06zAMcJoP&E<~VY_r9eDl|vft9|j&1#gQjVTzxmn2mm&Q7y$KxNnMHrge0 zU#O^|F=m)P1yU-gI89{7d%r}civ%_MY}mU)Dh7whbTn7I$Zs3b5uv3NvGLEQ zvSbeq9itSDXIhn>i4xkpe|OtX@j#Jk^b3D*>-&aau| z&1J99H0sXASKGSG4=LE%t1D3i{m06IjFw-Tu}Tb1V+0nOiem!%xv}l5ljWY=q1l;{ z_X~@gj~I~4zf@V=4u6l}qF4QWbg2PavM&mUzG?=LOC5UAPc;gwpHI`3QZGzS=Gz!* zth}e^-Rb}=G2`x zV<%d@z~B>iV|q@I%d9A~eDRyV7d9;~+=$p6s}CR`ncy#SB)Vn-{vLdHYQ+rJgK`r;V68R1p#>5I!Y?4LugIkwSs zO*I4UZw_`ftFQ|>@gGALD~^JCimK4bZ46&b=d>s$E3Z-ZOM9Zf`7l&jW$NT z+YJE6uElcW7lg$QKSd+f0CHN#G6g^d=u;8d`ztUj+UR=`gSyPlmyrw~(%=H%3N2@s ziNeGEX9SmX-zxemdd997q2N#2D=Bl#IP-L8Zgl5wgiZ}Al($^vVGWfR6@Eq^(ENDkxq<=wnt{MMJQpvdGc+1A9SG>ws_H=O{zpZ>wu*T2C@U}E!o1s z20F+?YICCbfEw6WETEsts=rbHe$=%}226+00RLCXv42Ou8k|HQ^ikvbUsl3S@o z1@r`p!-DizbQ$P(;rYjk{;#P)Ml&Z9shgFtBK!8XT|!a;HNnxTX7n642_Wk~t>k*O-xE34W3u$_P^m0Ze{XNcATr+@pEOVEsE<%&> z*F;N~ZDk$?n4b81^$^W8Tgm%{(mkOyqT@%KOD0)hIH9g62G~5O>|`Zk$r^{*+7@d@ z`6qY(P~(yHXk)|@3-D1StHl=gr`Mm_hKlA73w_ao(on}r9z&u( z3`T|Y_b9m^TU6Yl9HsDLLWyao)H>U7?P_hD{T_+wZgC($1&S3Kct`flFgI5h0w+*2 z30wW&;TL@w9uMnRk_Rd*&Q&ko3^Qs6qHe)9pSaT8Xb>9lN}uxFf`3cEjg0RbX&HJ# z7`rHwBP3Zsrcxh+xu_YQ&XVwr{52gfemHAl_lUr!SDSe2pLYX_3uUT3OQ^C{zLUuT zN#3V-r0+)&i#bvIK=?;_E%WL|GTvmY?yv7Pl`}_xCd<@?B!=XtAQYB_6U&IfMbrGY z&O2KtrInvC+9YUZI;7ctv)Yfi!8yPfXftJfUFjocrHCF6M?~%@_G~x>u_p; zd*1!w}LBgLz(;A{1~P zT%RR++oxtt(PN$|sJ-8f`&z53-iL5qoA!-9B4yGU#ha6!4&cP5H*AL;N~C9pDeRm? zSc)J6wYK6)ns6DHB%X8Luc+6ui-TWpd0gX$&G4ax_TR6q2keVeI>bjpa*nDF@VC0| zJk^+q6)=uGqTFAF0`jWgB;Z&5J@<1mroj8FQ1P!TTS+l2b^VGC%mbFEGeLN)_ETBL-=0dWAe#v*?eHFq`ELxJ93{dl%XL<#rEyy{|(5rIIyGB`e1V2mL zvt;w|=pt0^$9+K4$S*w#dTks_uPp=pB~#abX5Pr2plGPDiiN$>_^ZOA2*Wd*@r>oP zb6ooNZH)CJVGnU_YspI^EDJ{l)S%5a>UP^BT_km5o@o?e5n(ACxQ5EN`%4 zm#d>;*w4a`q<0Qy!v7GL$IU=9XB)+-o#L0I6mk@aw^$`(h>NqD(RS2^PT4t)dTB49X=Lb*I#IKWKaXEUEzTdy4 ze{Q9{Zsl4T;{T}i`}iU>c`z?wT*+Zn(r1OWAXqdc#VVuBi90$poD*z5dlSwG15Zj> zGo>1k8>hDNe^oAdXWc)~h|^5v^8F8SRfC`5gCR}lq(`J~LQ=(_y&?U&jT%h4e_OlJ zgTi*q|6(yTxFz*33u7DV(u*ur8h_Ec5yJV;V~+M$Jk&`mQ%!IiLn759@Zur(xc zEz7Jbx~PgoG&a}N@0WDAm#3*y&KvlP@I%A5cq?LcyU%dNFL)m-DxWNbmzOl(^i@c?U+w6%>9!D?(?RN?>CbX|rR<`u&ox z^_d#&nA!8X?`P%14+CE)eu<|)vk52<6>A1N@5F(lzyM>{)cPG1;O${nyYmueYG-<^ zhhW2yf8CBYNK@pNbIcN{fJBVHSrx^E%Eo&>f8$>FTqggFXlvYRyszMuvy}l?5I|m8 zwJ5Lp#Yg3*IfV>hv*EwF6`L4SFAjRYCzCTY{VJhg7_D(Zc+?87k5;#n<}ZSvVwhA( ze&{ag8g)AAXQt*1mZ^WewlxI=974^^fv6xVL8$8rc7T;26p=N}`K@frIn~kS#itP| z3v=J6?uCWJuo-xvY~b;cEPqlO=L)WhOJ{az!lJONw{fr{hCVTXwe1*3040}7aF?8s zu5^80o&#GG|Bmt4>;PlzZ&_$N1tfiTFXf)fb)&Q%59Tp&=?*6x1i?s6YW4Ovpi9NU zvTk7sRnE-?E88!v2m{StR8bzy0!L{1*y~{8y5GET-NFFR6%TIZGMJhqR}q}$grz~zGkg|Zb)@k zt0kP9Any4t>T)TclLX$OglKM({dn=G9|`@-)A|f&9%sTs zeOTDdkJ;bJQSyvsM^oNYGxHbhzjfWg*@4#D^qi;NecNdg^1-bSM*W|l#7uWi`2I2@GHIE)GoKZQbFgv*Yd6I!{x`w(PG(D_2Am85<^ zK=-cK;`ayJVNlDLWi?tr)oTtE|DE7_1-?-Wl462m^UTc*09^vk``t5TgbDGa;Qi1w z0>fw;5oW{+DLv)hF^+|EZ3WOX%hQ@*H0oSu*yI$&=UFSKnW{}z8Q|c285W2t*d>1L zFm6d^(rVs3&usHQ>2R~ab+itX89izwoB~=$yvYa8S5129}Nh^PV3R-{SXxe9AKP*skbgmpWK|Y zbwqE!SyN4fe@}mXBaj;NpFtTRtGZHA^I^%b6%z!N9x?!Eb{sxorx&tj14yCq+`zd^ zZfmBT@5RTCj~1r{WT@gatB-1PqD}_w6Q3o%)t7v|sQ#GN5fat&O>Z8Hl~qYT@;6ql z)aV@gq5>oCja?SR*|mK*eEFL{-m&oY-%5U^#AXatc?rW=4ZqD;MvY>pDPP;D0uDGGf-|OHxq%!qYPTw0I>fpB~GpY=HbI*unwzhVEu>p~X zj(^r0HT3H9;^5Ym$7=)>TNWgQu9s!KSn5>JOo%=nMZ6qIzHPe!&*p<9X1n~?dtzCs z@5I;MjecsV00OuiK<_T%I{dY9rR~ImT=dAFP^us>?n>Cz?*pbx3^9GHS6Y>Gwbe!V zj{0>c)Qi3ovn)^h!rPvn`$8F{_*p7X7hfex&V66)zQsI`7s__WwwvVXO28+?KVVbw zq{8=uJm{C_m|oF6N-OVO{LJ+`N7Dp)pt?dL;s#`u4f%C1vpC~teCgmFwhz{ZDMi0R z<~)dE{(N6J_QKcdJD>i*h03vEYaIMaIKjY#fw2b&bP>d=ST%c?s) zqc^Oq=^#A{2PJ#^Y)2_+5-gp?x7SrRb?C{KBdtI8ee-NSKm!UY9L=Az;VR8wx_;j3 zV@{9d0(iv5PMJpYB8{7A(aY`>#w)PPr2B@zN2{VW)-#EFkLr^4lO}vNvl2mNTQNxl zAeZ|x*$9X;`a=HDS(o&vZZ31H5)zgWs+rI=#;6r%%mO@PtNoA@HVbSpYQrbKkT3z$ z|K2W)tKG4HP$QrDqWJVpE(eil5hxUmSxMi%#-ja)<5T9m^qNBI=kF?xq(GtJf?)LL zq{VheWZ*tz#uyLUK;P$?t9OXSu8lv_jay3cbnga){{tbe_!6sB5Jto{MNNIzB9~_% zh{_?T7GA3-cJKL&o#ED^bMQ~#e#iC`N&4Ko6{E-Tndf?I6e-U>8H=`ai62R?%Y$D6 zcyvZAmqK)R!}m*B1^Am=%(3jb$7#(8x5yyo=0&JbQp_8AqOuADVIx}hlBkXQiB$PF zVD3n#(ySf)jlO$&_v!UBA@~Orum;$`8n}VdK;_}$Zu3^}S?d?nJ7vh}V{}Z)axId& z6*w$MA~Kt$h62TF?EiAcqY(p5!JODaaFs(esIc+5^{#ebbQ~vs!1XESKA{}8I^Cy_ zi9iOiHEj*i?R_nO@rf;YO|sri^rU(o!-B0KS9XXo*kiTK533G@Y`kmb4;Zd||D^Rk zo|29O`q4__o#V7cUy<#+HORYJ(fUKBFz%3|JgT_NDUCB8*m*F9@f5eloX7d~K<=3C z1AB8X$LlNqBigYUGDLgMOpOTMEW**>x4kKL-ZjxFUiyHB&^zH-7|MuMtKh1wzbrY*5oBl`mL37ZDtwav=OP0uN8T0m#>Ukm;9(2Oa7!DZ~D4?1x z|MvZ&;nKlh(oCuRWVyq^WG|gG9y9ct;Pie##K5Oa(?7T&8+N8AUm8)L7Tz+lCl+f= zEMOec>P$JIZ;=_{F%G!coXIlS8x|l)@^$R*X}r0PG_44A#6vgQ&o5PATsTJo#3aAE~j5 z$LF1Bd^Z@1skc~V2d|s@#y`#Z#ovGKl9u=(4rHk=A@u{$m}2q_o_>3vxjqr}Q2wZH zHPO|I$c+V~v@vM;18cMMd`scV%k<#}Cbr-$7DBrVgYtTq}QIXgg7_uVon+t4j$|+PpCvcLo#+Rv(Q!LYMudZdQbdpbi+PM=sKBiq?BIE2K)z zjx&vLVac>91E|-`POcc`4{!==V@0Hd za`%udWPa2o%^JGM;VFjvnn0#BvQ9@FoH)yY4C6Q&r&Hx*&^!I=y{4IKW0v{4{1e)Afzb-up)@=sWSBO#D^L(G6kGT!EZAa^P;YHi&?rlK&X5&0Vg)4aLgJWIC z(*x9O#9kQKw{BS{Xu2VQ)+FMDhkYZLi@WZnGI_`#deFG811PVv62iI29mdwS_}zTsFE9h z>xsEUCOF0$spLR-(0oFYLRyqfPPPIf-&^byK~5zHP6NsqZ0H*F7kbK)>pbTYwTHeI zhQDv`fi{fv*-AG7`t>#wEcN9|o$_c?HEOL$2~eT+kMagLbe)q$^A>yh+&`tuTngCK&=KkAwM~B>&H}I!8<*?Kljp{zIVUzMh7Y?O zoZlx21KP(MmmV0bJ(Vc$0pS?vMM%U#PISQVHJ z5I*J|yG`dDO_1)3XcJ=z0bt0Q*c3+umqTC2e@8t&1JFHbMamy9ynW;^^9N+&*m~_T z7!`?^SUFHz2qOQmE9(L(BMLtce;bL`Ll2JqgF%*Jk)Mo8#(A8pPSA<;#e%W8*0X7A zDQbRKaeB@vpx`#Dk0d_R5Z5xy9)_knO#|d8XKkSv<(Lf2-zziQjYGr8HO8uC+uqC` zTSI}YaPJR(7iG)39Vr_i1c-UIEy8Xz*Y^pu7Pc3X`z3n%@QHal4q7|S--H^h8a0k= zO@&P?zNu2aybYXq0n+`G7mi}(G3L4I z0Mlu;;vVn(Qf_R5L0avy0U95|=TNM7+-v#Cf2UXNg zAOC6IK5%2pQ}M9j#Krw9M*U*x&n{YR$wxQ~0TE7FoLI9g{u`ML%RiK3AMJZNY=@j) zSlLp5Sa!X;B(vSj$yJat49^YE*d-gVZFE~l4yjZ!#W^XYMO>p8!Nj*cJ-zom#f;gf z`~##vfs~Uc$sO{e%6zg@u)F215mC+I#!ktMCVN;l*0@xb#it~1#L*^8(2h2Hy9Z4k zI0qyWh2ISJz9%sq(s(C?nycZ{j<@j<^>of%?eI8~GNQ4DcQmwo?R5A=2qk8|SgJyL z!8_w#r~w^}UF8;T`sQ3?_U#wm=D|%4<5k6ntyK+Bh;s-de!k?(v{BBEZPxJj?vr6{ z@_#8RS>a7#ao2BOF9diV$I)JE5Rw-ChI@Qy#-yBc{#_uJs zozNs0yz6F-rV`MSen}J*%BDoFij3ZTcdYXs1L-c){zv%OKFz9tNx<8`IL?gEJ&mJ_ z1K+hOAzb9266<)XcXrt*#qM*==(AW*vXA?r_{r3-+;G5jmIL(1wS?AsaNhIRa(JLx zKMmC9I(!{dU=|zawt;?wUgtpiEbmvA0Q6jW2*9RWeN@)Zo9r=0G4k+N&c8L$$5o`C z>`<8#9X`)$wutzfN`F!#5nS=khqv5Of(A#{Ey9s{p_d(A6;>zxvlqfK!4(>O&I0IP z?7e+E!#05;1|LZ1a*%}?z)wcYt{w>jM5D7DR$O)=i3_cu9n`)I2FcTQO@(qNpob!4 z0)Rv_cYH~G(cP>GW-xJLcm6T{Uh{C`9x;I90^p*$y0z+6)CBo`wBi@y_;3-;t&Fsg zg0fSmB*~?SJqeX>I74uOi&qKsqyg`kc!a@#=ml;1Ji^lTB2dCV5@v=40Rn{U24@M@ z8H{o%!*SlsuH09JZbjr1H$Io6yNUf$jwPA`>H6P9yy{9F%lAkbB4b0YA*nW^>}McF ziZ6|MIkTiyAx;YvX(PRoJ`wd}V(o@u4PM|uQp5&N`5~U=8i4zFZheLibJ$WH$Ad92 zJjv}S8i9ssvWhl#xfXR$m9N5raoDJ8y~q2VU@oYnv`>Y%J(WX-RS`q6l8~SV{dp<) zzHFes<)*m9*|Wf^{J^+x`^u;(>Mu5YONgdrTjHV zcmq}&1wh=kg-o$8W5~Tf@dh4POxp`kuKs12k`}i6mG;>M;R_jJCBcz(BzZQ22=4M#ae|ny(YDRlZFz?dg=Vd5~IdY;kXt z)jR|BP0za8E2F$KOx`ugU216pKw-f7+I}zHV$t7sS6Lc(-|~NupIhkuc2%!ITPf{T z5(xUILo?s|rGS?vL$UO@Mj+L}5nQRC$Qhr_w_stOySW!66XzVv_j~Isc9&i@R>S{Y z^ZerWd|5m<8ZgN&i{O9W*5#fEe(OSq zH^sPK&!DY7z-o|d4i(07VdxftySLE(7Da~|4CnmD0-#L*8O)RH{$2*C@SaX^n`iPd z@4#ZMTAouyt1DS+E@T!~;~6t>A+R$E7$hy<^hCs7bU4t3YJR3NtS$AFr%rPvCWl4T z-QzMoJLE;Z97AaA@|X{Gr!J(Wq!B zAl3MvJ}$)yx?tDtT~HlrxSzj*t4YdU^XZAbVJn{r+2`Duqec;lrIV_r1Uo+U6?Z-B z5Pmq8blTu(AKeHZBemRmW=dvFUqmiqk=Z5i>Vi*zF5Nc zM7x9L%U`C*m^9Yez!C1_EG#kuE;axqcnyoJYREAC*!rf-m^ANp^FH-`L&tAo-9NVH zrgsajQE8uuYF2SIXzR`q?$vW4VR|5~R=Ne9)`(-aRs)g+MbBVWy6O|3niRqx;4_ zRz<70V{M}&=;hO(a`#?xuG8aIq$DPXT`DI>&1eQ%HHXy`O2Ecfki%AOUmY{miW@~l zajw0F@qxr!8Hubme!{zyHvH6C7LJOhz7&&oxm1Gd1cF^=$>w!p>J3Ou5Zk%GTqrEp z4vIAfRNx)>w!%jspM5}Zv_xU40)2f9mY18Ct^+6kzFA_e-VaIul0X`vY@o3nM1dOP zMedG=HOwumAT6Y>k?5L|&z1Jh*}%Q1HIEU@G}A`Vh%4qk%Hi7*Q0GYPht-dR(ZXOy z*Yy4RkVV(Gf+(*{peBtaq`_H>%H^e%Y1<7N7+;dFYvry+PG zJCI(uw(@@sq>O(^hYVwE20x)Kz$|kjs(8GqlWASFJ|Mz}E4I%h8e$zW3Ofe-ekdy0 z((yo!eDHdP758g-@R>*Slgqi&5e10Q#Q!2WoMg8k?Jg6v)#HfJq*i{CIH6U$QL|1Y zmlD~+0A92CJD`;UfV&r2@tCsbf*-s4CP9vsIUvR++~6M}BGTuwTj$cAI#NXFI%b#~ zePOSmr!4klJIX4Mh=|@^v z-3c&O`PNx!)FH2pQyYKdI0`{k77Bjo&X?t%)Y)_sz|ihi`ou-)ZjOlJ(a+_+h~Y^n zGXoNiA%^t_l2=y24YNKT{uckL2Sq4RP#MEdR$0ZhvlpRUK?BcrzDC_WW3%$5i&B5* z2=ssx^hd9UrRv{ccu3%~Fkv#wE0MkjaJ}clMN6d#5p2(e^)=YgFhCy0EbSLVl5uSUSCGJ30xnb1I!M#!B`C7clqgT{M%U}7dL#Z;wG_y&!=@*z_h>AiKbGh zMAyBaB`rbm=kbx25is<;RUF1$aJF|l(Dqp%~NO#2WI%;U+!`vCXJtV zz6o_MNvxUxuNpt{)={jex!Coaz6k};gEE?+3-VA_aKj%3J)h=<9kj83QKB0%sFD0? zYmE26q$dtgkdKKS?f_I5$%yup*-svbWU$uQ-&kFfornGlj#>`MB@bPw^Cv~NhtjMq z*&8AvWI&V}+z7k81~{1Fck+wa@;GtM0F*ym5nc-00!J{M&Dt(9*JVdj=ow)5$;kAk zmEr6OMQrZ&0}>WQE)ap_ei^`JXdAJ(h6+HCpMqn?o-y6-n-Fve1j?$A<*yl7BTpZA zfwI5G14NmCJ^`zYO))UCG@|r*A9FnrL%w~D%W2$h!=5tkR@7a1m`xOjR?w$J-JHEe z701DPBKI7HwbSq#NH+uMtzG!_5Td@LxvS{1WO6W(-+DbQF0d8~x0gA);fAK)Jcn+R zMT!{mvt|ErvDWtSQD$_BOoXyMmWtJz5Tc?T5D*suwZ7lw5YGBeVD`h;sdD9&lRAx0 zYoxcz2CvA#90y#FTSZ|5z+jT}-qTg_>D9q4p??KeMMRuBS~`Hy?;Q!ZxyFUG@9CH- z56~zQ&43i(yJBLvBaMKdOW@gS1YyzZ7|!^2JDa(T%&br##sBgAi|)D@8J(X$*-{ei zm60pQPQw3z=jCab{ke($WTokgiz_+dN?L|sMs)W2e~zgMU*Lf|XGY9zoy{n^h2eC; z`cTzhftP(3t;4Sb{w7w{wP?JU|9D)YD5dj54iLJGPGG?`Nlexqs~w5|Iww|dTybSj zH5@k6`X9pCwqM#zMU4*7#)iB0jgCLypoWyGtC88EcOe&Q25L)gnWBKH@ahI&FwcU7 z%W~{A9X|=!tfvd-lXJjXel&dyBLe}GZjfQYcSQalLu=y?nz$)W=mXo+o766%4NyQ-bsI z27i`=6VM(ya=$3$m_RS26?CM);4H$B|NR)-=-iyT2)<7*86_)VRoi7;m%xSq%qgr# z(LC?-$_R;jIT^I*%k_T; zW`WIjmtIf$$K_r**Aq|a!zqA*OoK`eweke1BPz9R;>4~&Wo|qGOM_QfHO8%(Gym%O zJt!Yucl|5aJEF?o>b4khA^GFy`KQzM*wAw-y3NaZ*$(C zi%S*_190eQ2?zhJ%#c6z3@NcLO2X=$VtBeKmJ}>yP4VAD^IB(C$1Q8I@J2H9&HpI8UdVb6-huARSUZiEYvH3Qb;Bg8Z@ zYjfh_FJkm>7jGoPxG&FHt`AqoBinEgQx;6j&Lo)>h93^}k{! zVn7$GwuVR-d0!+BdTUECp!@>1xCuyONk`2_k75)bNT8+%!W3Ja+6CB!gWdiz6Cq#Z zB0*$a#o}L0Bq_|v%r9HYDzoc1KC1;-zhT&d3>sK^pDoQ{*0!o0s&tfdW#j~@zpv&( zoWW1mzA)s`bKlYWeZT(U4sEYPf&ha#phD8?%$Y?`CEU66*t|*y%P{N}dHcAgg|w@s zrLJO7!90HA^d~Eyc8(b+U1^QsBNnW;ppm+kQ+2d?H)_Nhb}T5lEj)&|b;7`epG3Hd zdEPG;712G6xAVOk%2o`tgde9pq~Me!*Q2avTXEv@n||)0Dx|E#ho5t|>tiuh z=UOtGTT@K|Oy!UkW;u=XJ1Xh|qiQ?+&VLaN&Fi;@ilUJW31g6vYs%CyAg-~cbI+II z6N-%+aj-%q=u*8YC=m!27+_y3b#o5>@k*(Nz*Lab!!dpk{Hr((24bD;9>IB!Q=|e> z>b*IfP-2GIvFb;b^QY%MMJvA0p(!}avX6WaRQJPC7L4_?!*^3yUs~9PFF{R@Np6Bi z_(){oqj-GKOIed$$HG`~twLqY`sQwH-}B~iaK|?!Y%?cg*VI%=;DHB2;v|VI{WX{G znhC0_Uun(61+rZ%4+4<@CO%4zJ!G^3!pLFe2`gBoepr?~P;$fGpsq~3 z&i<+ehx`#$5->WENjcg?w*3phZ!ywxWq9i%jFAT3J8GOofSV}g`p--0p%+#BL0?S6 zAPRG)sC=ulUj;iO)SGcNL=cTkU5r@{wS$R^jI(~e{A7YzWnB~iUB9*mOx{HT36@T1 zQz{X38U;^3O*ZL?vHfIJBp=a5UeFf*fXSyI3Xt`rdg0k3N$(St%We++*?6s?)ajOQ z9lBZwJ2G=g=1Ds9&;oO@!RpiplyKTi z(a4Q5L8V!IKqxZL8N6OnZB{!JY5DZpJz~HyS}`6NG4^8C5PYUDjzD_K9pKGr*{nB4 z32P;kt#A|C2E+b{h6Y~a^sMwmH@$csRC&6!>CyDBa1)No$KS7g2b$$n+>ZlPHTH&{ zg2KbesuvLKYh@-MC9B1scA*I~L@gR(F3mwwTt9?a0ny#R=a!y&zW_$hJbbT~iQ?2~ z=4aAK&^z$3wg-SpIqtv2f$ODq@Bt{`?BTgyq6`8OvB)fFYoPxAqs^p}`Y<^_JvQNH z!=N$^jJZihxkS144I-Yx*R%J+g9@*TQcZCI)Sh-UKGNSJ6DRsMT%UA7r&|#*{~3or+s7)$7k7U)-zWoyBr6+J7xW8tRVVHe2O))!3u4f$;Na*l zjH1_=;ee%brAk;fe1q#${T|opU4(h&uUEAfQ_~B3sE1D*+jpiLc61E^%EeK`!hk`Y zP#5Gh&W%PYlg^lk93g~4_4yf~h*VuF%Kj%MyR7DK}y@ulwG_|m} z3o!t3sz_@8I+XKb&JZ7cV|N8Uo0f0F7j!QU=v?HDifhkepG3m5)39qKkp< z?B8Sx{Md`7-aNW1O=_xJxe}_X_Yp+6#%j+?QJ`vq!dzpFrS3Q z1OWZrabrRZ_6Fs#Cs024)0~RbO-Sf94=}V=Do5FH+QRdfU?{zgsh#EV3?PtDCy5TI zoW{{<&H?cd+^El&bL-8=NagzK@f!JI=5`}9ivoyTs<+12wLk{51GJxPr{g3!4eipf zl+6&DSp6TKRvwRE1XtmCph+#&TRrhWB24{M`(b8-(40u2FE}lZmI)n4%}vk9bp{tO zhCo2NFbVG7DN!J1-3*dj_>Q}GgD7a41?Ff{z~DDjOzKy_#g<1c|b~X zu;{!K^=r2ib&Bxw54w7CuZKkMU_! z`MNoqCBO6z`oe}MZ)B)_Ym34Xh=U*ia2W*5#=vpT>||fojRqPe>z}WH(Ja7>mVO0X zh`QX+91BC8LkuuvVNjfMqh_t!?qPg7F(%<%Q7l@fgMHlJ``vzI*u&yqN|+^~R6YFU zNkW7L`x^|XxM&0;u|JdbyS|Zs6Lx@!YyLYMG>Ryn6RT2Fb4YlcfX_Aw>z(Hm$-h}L0)4UcQ}UAhd0qlkTPCznQEQceTbl&!NGxSCI$G3zI$6)ov%S zT=W@~1;8Vx1}1e5nc>z~4tLnD8|5b!A|XjtwSYb*a9LRsHqXZzjRfwAiGJb?-WHw- z*U!ca$6YTEe@%}}<2ViiX4@=9Ph75u*4mlNcV|}lHmcjgI<##rF&ep7EEpDTOTR4r zl{Q5r?IG2L{=YG`KZ9Z89Dnzv)Ya85YF4T5m;pJ>q(Xp<&d>9Ri`EV&d{?KpV4#qU zWMlxYviIa1h8kAeCj*?#gq3HujjadvY4wvWJRRx#8PO075>D$Ya_|uvQ*=h++vl;} zu3sizGzT5b?D8_(o9-y|wykC|{5sXyuzB^S<`5Tfn-bb;jqXCr*y_s|m{Uw0RX7J~0t1SNL;JpO)n?CU_0C-Tv9kgQn9Jj=L% z&1xs%!-*7tftG(GiRsxv)fyB9VG*2&G3PP*??B(xGrb=X>sN|f&m{Mvh4?aS!z38e1C&>iPqjK?!`EBwN@Q;Y#uK=T_pKbEKP9lq?R$ zR2b|(HtlyX%d1x9b@If9^Wv^3NyQ>o{A>L&n{XPpb)^0gTU z4@`5WT31OAhI~L%@&|wodqb}+1U)V%odushzAVF$gmErzJ==B==Tx;Kg+h)7<@j|l zV8N-mqh1Xj7o``%dTszYlswqjl$X)gdCFSYs0a}HVr zs4I@kZ(}?ae(O`A*&BLpR?s7>Px{M7Xh~4Q6QN*6zAcM#xT2m>&i5%oK=oMnEM3X~ z5xk@%dqk7-5XhUM_bHT+u%7}z2azrcjDTcC#1^=6lnyIn+B<>3;~hlKloXLJ;;I}p za!*=*1Cy?h4>q2&U3ef*x_B_)o^x~N33}5IvQbg5<#Nua5DjY;lPa2tc8{D^`r#9* znW>uC+(@=gQVhJ>Et%dBCjC&?1hdf7bt@&PoX1FnE12veqpNe*a_Eoj7Vpr&pjRkh zAu_|QT7<8B#xLit=S=jk*?Z{J?oyAOHfq;}&eHe4x3x3fMKCZz0!%+S{`Abe#>OD) z6x@u$E9_8Qci=(CVT2;q*il(xAD`Iq%@>VXL{pf*qDH)Xro7sDIA1rjb8B>43Cu0S z_?OZME>vj!524l62RO*DFFM#HLRNy z7`0H&7#R&14ayFr98OE$%l01Yi&Av_YjQe4(3orc zs#Edap^rW_=TCSf6BzhFj13q9A-mLLyoohTdQ65!GH5zA9NX+vR8jRv{nD+-NJl8i z3(Ys*(NbN#%_^#AGQ+u*ft}cJip(bo-yI#J<Vi6pnEr@$i5Ntv><=IU?FPX4*GibP(q#qf8upU_>X(uC z)I`X=XZD6DtIS^E{zIhpGGKI}ZqVxxO|-}&OM+OL{;2iQN0@500Rp$~Iue`};>S6C zJVrKI-3kgcgPp?n32$OHM20VS+m$`=zS%8bd%sex{v2xZ22f0EkkhoIy=$D;37fWo zI7hQ4e{bVUEHuUd|M&5rzYNs zNA-u*C_A#>R|L!=e81Oo_iutdE_nHj%-cee>c6z`T*u#|Ierl%zr^Naf12y_=k)pi z5p`BkRc`;+C#7MNBDv{K0qI5>q#Goa?(Pmjx&@?D8dSQw8&T=*Zu~FK?;Yd4IX8}T zv3Z{LtZ&Tuna!`$LO2GY4zJeO-x3vrwfIkpiUZGxcj;Z$$CDCMA<3gso91D8gV-Lh zI1Pb2r4R%-3UzXI7-OxY$zePC%sugSH$wX9Xa|P4&EkZ#`VG}TuIR!n6;@0K!=zDV z?2Pzci4gTg#X2?`cceHkHz4uFzylh125%1?q4Kaj_E2`#2hU#O;NH#z%RyA1Pgh zQd=&3!&|lr+j{s1!;XHO4k;K=1Gf+`8OYZozCRv?rfp7<37s+aSN`s#>S8qP#{C=d zKAO`*;p1T1pw@nMLW0j*8#!6>ENdIfzj#vFBvpJhcIZNKxZmNc~#BP zD)J5y-v-*&xE+v-(#IzYlXdWJhGyTw9V3BWvyAn(hx_5ud|<>k1Zv}vG8ETHL#w4h zd0t8FREMt=!uWILQq{zNgYG7P60vW*x*^f|+S&vSe!4<)o9xm9-zS*$(fa;mAp+cRV_kM$4NU*);}#eWlpYxkshT^3_JJ&e`8A(N#Ch z8zwM`WtFftl|BRY8{dH`X5OYQp%!0L-B)FAA}v$0BbWo3X)WuEiFzF#xQI9K7DE=b z+xIf{o=o|b=P)sQD<_EEV4BAE!YkEzClr))QU>g|>71}A+$zd+{vX=QqH42;oRHP-HKnxehK=g@ zC#H61w!uae+oct4_lc-`srj1I%8Q5`{&iyk-?Kc1mH2SpC>@QpIx=VnL~fG`1y>65 zTHjBtG88UBznZwEqviAa__kf@nKSNPgG1nTH>j@hwq@zC+wn%wK|<;=4r)7AnLz~0 z>FwTP8HI(-0wtt~J(LalZFb6oi4{;jT~#;96EOtc<>Gn<7T@<2|NMjr6>G*%wpG zy_SuKk+-h8AFWhzzJW_9`PM;Ue+KgO0T_Nk4;+pn=Nk2@+d^(y{O`ApFL}w8`VN_* z#LeU7BruTu#{BS_b$KYVqINK{ZU60ctp=Rj5i!Zt)ymDA=+a;%|3s*Kwp-l}59K-q zxIxbt!yy107x@ex^ya6xUJiahV9xyR|>w$wjs>#-$?83q;+{e6zN`(|%_b zylvYUPh3=@*Lc4;h&f~(QkbCJ%|TIpeCZ4~6jqq3s0VBmbqq(&!G)0=3G8nT&pA(T zA_@WYdzr15=yx9rzwI)^g>+nqWT{fCRr3o_(NBdV3nJf?hWbU&M@(L^pzix}|0ElJ zN?R6Mo47B?3}g2Z+qEW3l+n3ZphBerg*wu-lY@iy^C-fIS`Y`UB4imG(!Z>3e34M= ztrcMFdu?-u3OurTTjtx;3LeszbP5N60E!e5OlYphkf=QLwm9p6I<|!s_%tr!WGGW2 zgh)Dn5W83c8XQSqNyb~*siew0sf$#(F}vW{pS7$9?27a6jqFr-bm1yBXi`-DNg!#} zKmf|C@J_t~el$&WNMcjaFD@VuwZ}ijbVq`|2#gMZPp`Ie<-X}qSViO>5+Mpf4xUJJ zV%iI`ogO1U<0LDr8fTzQ_h&YeQq9e7XcC=oDA~iI+XZ_b9huS2&Q%ClncbL(QA5J9 z6WGmPOXGGmh1_0nxUYKpJFsQqcxs5!37Lk}zUWRB=P>-OEl`o6HnZsPo4cI+x$?qYv#&$@Z_()a0F&A7T$heCRQe+#s;7 zdcZbve)%cB6rirc*Pl=0Cqzr3PEY~?#}}r#LGFHk&p==8KH>Q{64BWtX5-5R#=v6a zeZ?s~IBW`)5ND$!74A#U%L!Z>SZLr|TTZL{7se3i#fvU0c|Wq3rcM%NevWul=x`ev zz%)W(Zl67TN=b&C|EsHs!&Y<7=5JXlP)&>PSPz~vCriVewo#bVP@tNanSU;0iTOu; zGnJSVo+;YTN4CdH9Z<^(=skeEPmdg)ACA=`;g0wUpG-Kp!qOTHK&Fh&wfj9;uS0f) z-b%V1d1K7KEQsS;>8=9IrvuV!Ze!zp-+X`7E(B67*Q)29vnvgKX*M>HD?vO zzcz`zC(TZ~{^mnG~*XYxu}LIMbC`ks~iq zFqh+M&VOvmJz|b*#leDcTm}ch)5G_QD0k(w zlQlayxSIs1A|7Zvrtx#;+mzC= zjmQ#uDRIF0^*}eHF0AB0`3Ol=$KSgu*8Lx|m$N&?L<)`j+B8?E#OX#TH>xEu{F%eOnRVj?mJT*<H3juI#;5Cly<()Q}Y*a=JgM> ziDweQ=hq?OTyhin(uU?0XYX4Mn|41@%}jZqPqAc#(V71(TL^@a3``L854&0+p+P_a zN)?z6avI+^d0X-mmFsQng>~|n=$JzFyK!qP-8uH%t&otiZ`DkVps!Ma05cnv^5z`0 zIztk`tnv_C4bCSk=C(IZTEfm^m*vX^?}WIInM@x>UKW(T9G|ed(_poHPUQ)pfBk76?c`kQcObut(JWQ zeqXamKOKKeN7g$=4Lk!4{$HHI^^1Mt?uG#kWA869D8=Wm7q$d|ucreI0!Ak#>JV(8 zRsl_G19zr#olECW3%P`{GqNKMq6yWx-R3gm?;C0BN;T>t>pa&)@fv^rA&1P<**heh z^bc-AWK`~MYI8M+}gLON(!G)A)nq58%2btDEC5iGfDZ6dfxh9fG(OKk$gL$n|HX)A1kHL^^V*@>%V&JGgHRA>t${h?vUup~SygNoY-Z}R`hB!*;ocTu;9fMtEQ=94TbpfwJ*5~UEY#CXd zhU#R}e&e@kcNlk*cchfIe;Jp>#fchV;xu_~T|P2c;`IOz<7__1h-5_TrxYlf`Zx5|c-B^>jt3^oRXLxOT z(GLQW0ZjhNb!zNP8KxewpyR(ju|3M#bT_}YcUMl9o=ezv%N+)BHvDC!w!+N*rWUt{NJe4Ts7kuWk2qbSz}Riz6#wcu8y-=H*43&=F7!Bn$M zX`@a6EIO@4{JloaD`6^R#RxK#f`Y=p?R7KPs9Lgp2DVwtj?Q8cIO87es!raKmRP53 zpu_?C!&I=hjGg#y^RO3wjRJ*}!O3IqqFwx|-cPzTc;;=QmsWS75tE3+GM-^Y?PS=o z@&Tz%A4%HRel}A^^a|$#@JL?o>D_FM-sSzj3zC;U4hmqEbCX0sXI>)TanMBmv?pSb z{pP`i*Rns``{GLpij3jrK$OrLJ;*nqrBlE_aq_UxqZLwY!%NifWhh8m*skbeU!d10 zY^dmVCi<0SJ#%S;$yDVG+{Dy?Z{B~)tg(eH2D27X*ygG6wL)G3lH$?Vk`@JxwnvJy zszh{;1m8a}sDj)Hud(~ZjY{-)2#Haiz}aZh(GTJL{8e4NFyC@er^$yv8t<$lGtiZ= zEO}=~LqP!`hd^_5;ala>uxR$HwVoU{j@eg4$M)T zuvftb9_$lV*z3WfNuZ+tXhQIKwDyd*=dh>%1DDUBD@Q^?%(g)~JgTH>^!F?Ok?Nf*Y$ zD>|e;PpWke4HbaP3a~OTDM$FN@pIfs_NKka+A-M(NPr|e9rk7q#D2qO+(`LFByJ#R z7#JIw$MyIw)ej#8pgdm83_F(4OKkJ%{#^|olbf%{XcyseL<|4~-SuX$eYWMj)cNP^ z;E)EiQQ)uRtt*$0u5H{bFN-W{l*Jv+=Yxp(XcVomm;}d4Jbqp&REunk|yNG_n$&eYbQ86{vWgfrbh( zNf0WOU)=$py2tr2sQ{5>sB9yae&7_A0O>u-eE$#OJ=hXWHR|Ml^N*@@Wi(Yd;MVu; zw1RacET?)K3+W{eiXitS4B^$ug0eZ8nZH189($-P6~}aJm@h!*|8?45<~=Gp2HWbB zCvzg%RMwBa2!9MBUY1**NPSjIuQGgPd3kviz=%3H7*WVvMuyrr2!Olxio}cD%$8tN zOS&SUrSja#7;3rc-N+hh0AD0dBB|YLJ=|B^Fd`>`+AUdngn*k4B({M5s3_%?1h4hQzymBC4f->l823Y} z?C*3U6m3N3B1+K_-w=0VtYv`->{wb{aR}XD1&LSh*%@+BE)??X{Q#DJveguUf}6%G zTB-E!fp*M?x%+|-=4@HnEGqazI?x!+n`Zkby1CU~J|sde%Z?5=IM08VZa5i5J4ZtB zeG4jMUF`Tb1Nmo`ovGN-Q~Yn?ydZl*Co8~R#ps9)`xbU?H|(%FBVF@fRxRcg70awD z?YAl3&BC>}$6v{Fnh=?(xeH!Ea)HF47ZX4Sh-kpxMV#$4AqA?a=J8%e6q4hF2$Q;4 z$YIIdKS{^A!oQkrf}61)Kn?a5OOMG3SF|Sxz^Tsq}8Zq&Rk1tpApu{dT#rj^Zt;3V^BCR&~<;Y8>=36 z0}T%~%pzMu%%w00Ni#u9p5bi6|2Rb^;pW=h}g)gQ?2jB(7)qHF)rV$w)t`9wAuM|W|Zyq%%Wp84OgL>etl)pSa z4Zrpmg~DChm8T`k zyV~~5!i~PScbyCBSFm|ZC(mR`eZ?Ue2*MtWGyJ(#4T!H8^PmNn8qIWc;iW%oQ82;V zg;6gIsAV{%xJRb!qW#lx-7@}G8kiwr_EmwyhD4=|I`0>K4 z7OfTL!~ScSf-y;Z&+N&>_5_E|AKcM#S6!VG5-@!io=_RQTD05ik;@6UxRS#M<>{28 zm;-^78dPzbTc(N15*A?|=+&2I>46YfV2kK}f*&gC0bI6;*dGdMa<9pW6eMLA$qjm$ z>J^7WG#u`u%UymrxC!it=$k9LZFXI8;lGQ2O}~5I{|6}`09ZrV@Y4M!8jYi5LSfvWhbK{6G{W- zYqsGAXo{db12!Us;g=S1!?PkL%Em@xe9JQzjKewEn4zy#Y2`|Kq788I{0-LC8q6yo zmq+U+-2REovO4HMgS?S=1`P3Z|2tyvaS6zX+2PZduR^wsQ;#PaS=cGyyFY&ni(lRO zIf0NJHXV|9GJN$Cd|hACv8|%`YY)}as>6R0TbRnw7!jDiHV-SSRBYqHCqzbUj!WUp z7)&~D&Q7(jIwRGb9AfSjVP?oQ`S?b)xdXW70je_d7#gCQK5=Ng*BIR`oc5igqUcS} zClt9^&710f0ga#qvx9g|R^#~#CMpM)83E0*Q~;PP&FYRX&gA6$Xd{yP{{9+`Snd1i zqo^kAtUt5ys>A3{Yc;JVpm_&wUaBf5Nmy{}P3+NX@_dJRl^g(&-$r@1iz)14ueX_rs(F_I%8IEnn;lt@0XN0y4C5-zyndkmOo;efeY#MtZ8&lq_6S z6S`ALAL>KVAxok(x(`^Q765kKfI1(mY`LvbFZ;)ErjF#M5-p$4eDUj zkq~{abudxSEyJ$H_1HOOIvc7Li&y=lwKCO`f8TGHh(L$*n(AH`(>Im3!lazmZ4z%G zML^V6f7;FO81);{zBjC)jUkNCYS|CTDs(>#D_B!h{?_a!^y{IVABZx0O){b|!nXjk zMf~t*-anj6=0zUAq$>c1A^vy95P@peliHz)9~jBNcGeY~g1yKE1Tq8dl~UzN6gQ5I zcSNe54a<2Is4ctzpKv`}@J12sOP`rVvZ{Do2kjyZ0xc)0d{BD61Hu+YhRu$Pdb&kn zYDp$|>G8XuJ-XPMwJuKNxM9__46JqXNs1`r{O^tR=`+*lUmC`@kv+T*+m|)16I7&R zasrec$2e#V4%Oys$<1uB6**)a?cxV!w_d~267=y@J-J@vFmBqZJKbd0wvY~hmVcbJJoq02ocxrhF%Wwevg^5YYcwmYf>D6CE1-pYyt-G3(xt>hLN28w_y zrl>t!L^SSzAjRL$W$F1VV-KHE3-%!D85emwh)BG5%)R;!8}Ih3>@Tk%Jn{F>jwe#L zUQFBNpo%|BE5ET)&^2}M?57)kj8v_7`vlE>)$`_!35UamAfY7CcZMGTKXlEb&Zn;| z*cv52!g&8Q?r3@>^216QfLxk2H|04HjI+?F60(3xra6cjhXZs{OK-MdmspG?T(eex z34Yk*K_LBoDV=_(yhl!BIrHfE>#0$0q+S6BC+i?CnXi^nVkK0DvXQ`{`HHy9kpzW8 z@XI$P>1qd%DZB!K{1e+RJj=K>EGTh|f69v)iYPzj@eaU<_< zSV1H?2nTj_5VB5#w2l@(fh^Jk(j!M{+Vww%DxtD#TzsL$EQk&(((t95C%kei-hor( zhtQBeQR{HKO)=DATlrlCn~nl?X!|VtbDX)fuF20Z7x0E+eaA+xApZvaV3{9BZ$CSZx7?XeZ3l1=6OnNL#7*w546 zWzisp(--v-9d|9)Q$@K6aQh>1W(#K|Y6&sPPA+59yVd9FV@s{A;DOO{| zqA0+)^r0 zR}~Z{&qK>E%J&(9Av8GlB8-2;*ZGx(1xu<%%AJFG$b7$?8j;G&4Tdxi0mJ{>tWd1t zANNSufk2iEnx?#6-3{{>d^k+XrLB~enqrYZe8iCfDmb|Pe((6%$jN!*pVlJ)T+SHJ zXT*o{0*_S&8fj86i3-TLM&>)Vqq1FTn9K>hQfNg-R!#`Wu4=}aMih}AbS)`YBWuh7 z5fCH_(96uK5A5sT@4$3?9CSz3Ag-N-0O19YYg9&MQOYq{+liztv<5k^s#SuObL}<; zcN3?+9lFcM8tqS5o4lBk#^P+xYBDu|T1RH^1`cE&W!bOfUu|V%mn1}hjcbCb>sRTz zr6v$kI}gCSz48p`{q&BTJM$4Pi3YZDth2{uKmsnVHaH1q-y`PO$S+o;R-_c#27i!3#O9f0)b>BfO5^wuP+>lzF0p}z&6dEo_E$2<{W;4}hhcESn`I21LEOn5#xIIQGv3p`R#AlmVyziOFdy;A?%-eS zz_oIYr=U#?A%g+18zkVzskeoB%uZvKnMqhac-i1R?M#@5(>hb@(8 znrM^cS#F(qH#QN%jbd8w8{4|~X-g&U>BGQZ))(n{=}L~))n&acLF@Whn-mn0OG;Nb z8vR}!p2-v_YShLi;(t@1PX2Tt)*ofZc>Nny48UZ0OB9GYDhILUG6GtnIy$Jn#Z^~THMxXk#WFWFbJXcSt@G2271X7C!2H9 zu8w|2!*k;o(}y?L2q^{O5kbhVdQ?>&YVl7cuQvYnpJm^XT9kwrl!CnDpW}hW?}tiq z@cm1Y6$1_05V4JuD`s?a9ec&6~ z8Log-X~p`t8>iuaTWG;Of(840kC1`KyKmpe1gUAAnd6)r!(maxzu2%-_FIpPlfaO~ zXN)N_H`$4Q4wzC)7}=+(5M8;Akl+bzVEgIAzm%&l&nv8m8&GLKn|_IyKre;YBWGZ& z*?<&iWiH(JTCRGyk$xYQIYO`K&*f@5!VefOrs&2}rifXk^jVPDW-1 zn?<&G#iWH-qU^VT$N4hH&K-KIrMko;1XEM5P&uFa-8IXZQ!E(ThEYt>Zx#su z^q=ffZU*0YL7wNr+2j3A3~M=Ws5BXX^Zw!X;T+E!+i*?HKP*~I0YdJDZEu+bu);tv z{3^ajP zp!gpu7e38XRNxW?e!6KMeezl!|MU~J5rZ^-1L+A&<1g95B^1_9)GE=`3Nx^k{0D;n zngc{3K`7RMk(PlYWx!};kh-+xx^zNhYK<_TsDYkM5-6{*7hEz1r1B?-OM-L6n4qR> zLw`PeC&(a;-Ee?7R}c&E`|S2>)W=T}_GuS6WT)H{B9vJfuYhu*oLZwP@5^5flL4=o z7VKhMVzb$_>Ws8}>b_l;1WY&?*}p@LJ%)?*3V8^f>amxwVpeL4jD~?B9X#v7=m%>V zTSg&QKbgcxgU?BMd@d6XBQ4Cs0D?#I!8l)>oMqAW>k`Fh6EIP)WmVzX+;EnCGZeRd zV`>%ETt7H*BB7@c6n$3UDC~w^xM!poSbl($-QN#I!?8j0pfCV6$A2~$5;7D?8&ymE z!jq8LmZ4ErpZpK*@Rs?1MSBTTZWq%h8e2@85?R3QgC`kPO{Rqcu}bGfH5KOiIt25X z8)40?B6D>r?(<~p&(IL{-7WB&Wd}kc9n1vogn~MX!a;b2D%>*#wB}HS)CBpVI+S8< zdt|ZN@-@Tl1jq(JDqRO*=_3(6>zr*d_bvl)AuU2>q{Zl|~2z@_{Md(Bs>I=n#$Qfli9($UPFE zgP}*+#{~BT2EH*3Eoy)5`z|6Nh*EbAZBg}#QTr^mmlG+VhGCJ-->%{Tqc{&`dkr{U z;De4x5u&ssvv>q=hKZpK+Y{2TFTk`EXf8jj-P&hcx=0bnf25J9c}NeWUchyW=?fIq z-yk=EQmaKYO=!H_v4YgbT{v?^;yS5yZ0IQEK4r%v_pz5>u-4uYHpdh{P2Y3Whr+NH zHcZ5zO||&__jZ*;W}62J40rrfr;EhII5QL;EOKq$&DXxrxhkGoZ0TF zSe9ZA0GqT)k+W+K5RskuoHvW;^`hB%w3f`sV@ow)qKk1yv;QjBCr zhJ>OBt_o#@aHETZ<<4DYf7?)6^`6h@Uydg&flNAM9^5~LU*RLkGm?&BrO+c$e1RU| z{)KEB6!nGc=jKfT#8in)wrM2EBt`R={X@G8!9OeYo9wP)&$SeDNe->sjE>5sgh|RNQehN=5;_4#~E;=9N!O=gQU2ifdRts$R&lV9^8iX^2I;ziYHsf z%)ajn07!JDZMG`{st^68As%4D4;2s?p9R~HHs?qu{MpYdkd)3>48;CS*f1SVl_G8$5y_~L2gQtNr~>3_OO`l8JE2F*r(Wx{4mmcy83;*xYNF7;+` zE^#xi&P8w)(BYAB7B(wXzuG2|!4sAw^i%c$@nD5!^>O4wAv4cNWbBzz+;CX_6tl(F>wax%EdV{D^>>*W@5lg5m7TOf6nnB@H8Y@N6po_iTb_z9`^no@R_Y z*b^tY@+G`i`ZX zi5DrmqqX@!3nHlkEcocH2Rd*M9Yx<F#N5Pi;Rl zlCf_(C6!$QZRp{?qFDc#TS&ftB{m@zsqS}$RYvG_-5kjgD!Hh+kjnQNNBZp3m;5k1Gb+h^L5XQb4+EpmIBsigz4NM zk*gY0?N?=;3>n?X)QQWD3KZ(Ed_|}t>HoY3LmF(HXe)+}_L)z15+HZahCFi(%JOr> zHK%}&A3rQDNs^9vr^3jbbiM!=$Mjr;xNylzF14xKBmzo-+$Pvler&cV%JKdhhO2$C z>!vW-8|SU??i~SO?Y9g8{)Gx*_qCwFJjDsA=pGEnp%I^MpfWHtAWiJNTeApX&65UBx_9cYK(GN(p_M6av7JZgkaA+>064}d!0T^V z_nF~+JSWpQ!zH1qs#W2Bi7jA1gPZTew5DGKZv>`HfCwo+_f(^FKAvwsf1B<6=Po&i zb$z3tQp+;auR4U3esj!|B%Xl=NJ4XlSD>U0EIynqL%Ly@kVcTU=E>LC+(|CDb(osY z=0)+~%|k7H{N_?5tHhpsP>yrfo^Wz?kd;uu|6H(q`LB7x4a)XJ#(PL1>!%Saga{hdHte`KSp9G2r#>4x(J(n!pkbzNldHY_ju86 zO*IvQc@Gm^c~rn` z#wNYo@Wo6w#DaLbUIgr+%}aB|2txqEUL1LXj_h^WFd36~y@mk*^j*}FCrjzZ`ev_o zJ<~zEm(0$VcN+y*)N4VpI{bDMO-M7NJrT3G9J@5;4N6 z;vPd;Yue^Th%{tnL@&!ELs`WMQY!>IH_so#zh}OBK`|BQ>T=E~P1oExtvfn*UDQGF zYOQ>o#gT-+GAk_W_4GocbrP9%WQS>HDRbykQj4n+Xb-ESR5#2&w=m3{XUh4wiqvzo z&l_0nz3y_lW{)2Uk9wkqZd%zi;Y`T)_ysMuK4R~GEW|9~%_s*;LEQfxEj(r#xIAdr z?j5?KnrZ6sWWG__QR z3h#g7!fHI5rD0Gi67j|TJrm~5aj2_hd4L?I8KfbNNK$*H+{QEzV4AQJYibe9uf}MB zHg$}|4sw^vxTNUxlayDA*3u@pL39%Dt-W&;wodJlEB|#Q$7&*cI28ai2NOJ97YzVJh9KJx3VlTOn*RC%zZ|l3%^~4HHu64(q58tLt-yHV(>6!;YPaZSL zPqr>F5L$G|-M}Sd_}?WHDIs4V&en6g1^28&KbR~pw9LLf9P|*6e|uivi^J?*`a^V@ z8)Rq5ESln5ca=p1NO5ujngn^KOK{6bO%*5A_t}jdT0F6EKD;?vKHKq-1}jEDND z)~mTjVil2(Or&4UmzjDaj~noUL|MIpE#AS-%&dj8QGBzE1uQUa1d(&tL0t z7zJ$_kYL(7$mhjS!v?yQIp%%qU2NqsFUmIz3Qw#afzXim(=xpTwpw*^qL6eh>BP1s z5UvTJf%o7uI|lpdf%Pmka97b6ukUDDkj4uflv<}M7kS(`NDi~mmoB?;F5S+T!e>RI zvuA-A3TojzbM>=5S#NyqWAMI59H!lMa8gD<3GS$Q|NYE3w!gE_{XEa|%R}zY`d4rJpvZfwT!l6>q^3xX4 z%qD_V8f__SU`?ja)ZMg=Ps6VF>zHD7CrKhqpjk|Y0ah_!4u(|Iz>8hC{~n%=gNOqh zBM39l+$?GGnhAqfQL8NY<&-Pvc4%XiBEz=+!z}rqQt$;(7jptMTE{hZIXv`-Ot=$2 zrtm@_c(Z}f6oiiySyLoe);^`5T(zr>)6mz|65XX|e(Ybvg*^d-4w|Q<@^*<0=bXoo zJ$~?s`%fQm65U~@(1lAj_GTL2JKD?V-5uTgtsT-dCGYA~KEN4KmO zC+SNGaUBpfDpmoBg4M&{OMdm;IHyhi;XYVRBY*p3uC}uNL{&~jZH;V#vhN$OS-ZC# zFG^G;D(PcmI}aBXi@7vpkxXiAXLsPBt-t>%uw>8d!{LZ+Jxk@puwf}z$*A*>;3SPw zA$DV-Ksvz#dEbC8k}nVHl=cSJ+VQdtMUes?I*!R~*fNVsRD;q4|DJbgFL%+_+tYHu zdsz2u;=>%rtiS;`Hh`tTbe15T^av*ZQA`gyp~} z$m0lB%5PVYzz)O$VGB9D8Mi>L?uZDW=0dhTc@>pCUbbAde0WAbRl(3K+BUnz7j$}| zN@S9uMtYj+@*cp?zQhc97e?YGlcZQW5z>=u=SCBQ| z1zT2co{PsIh6MSn)@%FWaO(0ZK$8~nxkLifNel$-%~jZ@fae1h@ZS%C1R>+#ka_Yh z>vvG)y5L#=y)Y>{L)5fncmKdZnU3g1`#L0#vHA~~VB=KeAxL(GG(;kTYuE-l(68Ya z+V}ykyD(HETViI^DeJ|`$FK@W6;R?XVQSqo{TBx6k^Ld1;+Qz_8NCm336k?O%Z#<5 z^oJciq*j|gqdNu$Qy2dV^=i+-TzQ;9*4>mJjfY7hmB({;9dXIUQ z-J`U7UsCzFPA%3v0P$P)%m1b}MUb~$jA_+g z&-9Nu=FTP6I*HfU3t6%el*lQ6nHcmc$lb_6Mj6{rH097qOLiI>9B}`N50QZlet|lV zHa^m1W}<~QUMq{}%Xf6SLyP(S{f{)iDtl@FKri5B7(O7J34=aM^OQ(c4lsE6xkSn1 zy&@R?RQpjm@SkritlrTQ;|Xh^gDcdaQDWy7G<#T(A51l=2{gaT)j<9S2(Broc|bV- zt-(UD*U291q)N_dqaoyA@UmewE`p}4-TxZ*8B2lCwVwF*NVEgYaI#td`(=Ia6D46p zd5v;FTrAkaxh_?!%j+tHa|Ne8@jf{SIlkzpfd%sbqbffzSm2d?b0SF7s|t&F`k7D+ zuDJX)puLv+42Qo++mI&8ezWGl^Cs)(D=gY!^6OMkdMjxaM+BW5N!q}=F@lX%jOsHO zxIu!hIWR=eKP)9H)mZ(K0T$_d^Jz18(TM$x9uK~-g;v~u&N$(|tCYh6*?b6@()@EM z;C;{%T*00(_-b%tejlkvhd-UcF`yKU3+I(N>A&{xiM98`GQ!q4JO*vV=6@5B514S# z2&Xo#VHz!KiD4O0i$UtSc7lOe)ZsLRBr4~B8|_brH`#!VX8Q8hqx&-jdjK86*c>S5 zNt%6-ENz@>H>?LtZ?0*pO)*i?f-|Y#8<~MYXNPQ)*j+%sEr^5VjZpcH0W2!52O8dr z$Ui&l|Kbv=79FRmP5$Wf#{R-aUhD9)9ppf0SMfwVG-G?G_{o9J5y(i2Y^$|wVW-*D z$vKl7&)>}tDPNo0P6oL;g>yw92ML0Bum8YwfZGx(4X*;G-8X0dO+a75us8<&+Hgtu zLRsasp(tD(5KL_bg;R^8C=C|D-lu$Ox<@qESOF-r0j$wA;a$?RgiC?(xep63^~}tL zAzVR9uL`0M_Ln)L89r08QMtF_1OXP()7@!%&1W^6sl;H{7A6;id?1eN1DFe)269KX zX}j;Ey8e1`UT|jqHqn+xPMIvK#$ifo>GVqkS_@lRfTuuvMVsFnlA1ep5gI#v$?)rQ ztpDMp7~?Dsu7Xjyw;K^N?+&;*21st$Z?F--Jo^FMTkR)L53RFUb}B%99sc1NMnjS< zkc1osW3H7@9i{fAa_pTt-_>Sz=_d=Mxeb3(izMmZ5AKn%OIYB(Y*}cL#FM0Lq0Lr> zi+@I0cJnOfm9TX22c{H|b`LDc4g7^v6@EhypZO(XQAACg6tQmMPH5yqTH>lQrb1xBiu?mB;<2~2>=z5K|22~e$X!v|; z`%9KofAv}Pm%IN=5bzzx(ZmI;X}W5-{*7DfT}Goizp{XEK#4o~_q6&iI5jg>PQ=v% z^ih~bwErEoqiR~KOk`@~=;_&4Wldd5Sue)}6+ZfMbW*sFbxhDEK*)?U4OSWdMSL2S zr#43DnB&-JcZ8VY+k=$YJR3&ZPq;xGG0w=ty>H~Q;aL)KM6r+~+kGC;n%cU1?|?Pb zS&xv?O)*Tmqh@jH!~ZgdOn|dhOdRk@j>3_*(!Q}Y1~VQxJ!A%F4l%w2H2A=F+kY=0 z4jI{ih_jEjW^q`c0SBOBi+AX7WM`^N1+zfmNqjJrFk49DPZ@L&7$E)}CG*hj{_j0A zByMaPjOIW6SDhOXUd*r*N`kgSFDGxG|6q7ThQzk1+YWv~dYB9g0=wo<4TBQ`mVy0; zNMF8hmA)BY(c_lABNozoCkwE&Kff1_t)wa(x^Wk@^+m z*e~KyD$bwqK2G6c2G%1jZHvIlr@l$f|+?AcTbYno-8`=K;yk$|KC0eah0k<)eu4}wqWsKrKn z5p;e0>r9>-Dx?1zs?eZ9eNnvUehN<2B-MV>ElTp$uWlShiG!~;X}S|!p~>iJROoIB zDA~spRZb60kNB3gopM3inA};Ro=-W&o=QkB;*Q zL1-S=Sja(R5VWBtUQpnHMO4^U8+nPSK3hNDs35wNdGht02EMumfX0*j;qS~(?Eg{c zjTHED=Ub>V%BqzjWu6E}m`2`s8b)*=xry~h_;SDO<~GmX*4N7jFuC4!HpeNE#iKkE zvZ`qmux>GQh!&q1{(fnHyV1WO^9EXIV~#?B5H<*r>lEY?Mfy;u`MsayAE(==WY{qW zJ=Q{Tp5arA3xZ%?DWZMD73IN~WC24VqsZL$cJR9MHNuUx>3IjNE7z;;9!Jxn039 zP`@~Zk#45r74QtX|K8r?)&$Q`>Fz^$=Mz!Oto7JNoi`prmXER_^|}4u8J@DCaHkm$ zhmcP4(QMFEYe3VdZ-2ip4etTvGe)I2Ora=FfrraB$I-H1zyy|n_pYb#zq{D>wcRTJ zeudGI$pgnws^a{ou{ z<+cZcIxAEP7hZS;wY}J-dACu6^&>c!m%q)gTl5R`0vT|<4uYQgBP2IyJUl$2>bli@ zakD)8x?LA?+T}Fp%lOVr!faBElVaHi>1Zd=o-r{>HLn*s-*QRrgz^`#UZUboDt1-N zxk&7wRowBNDjPk9kQ+V(Oxn!+{%E@W;;shdxORB58$ycScmF0j;GMckj(<_k+2-R9w5OE z3wtV0-|+Bib;8;o&)j|f?Flf?=wBE8>TI#QKP1$nL#ljodHVt0Ae7ZcxU+1$R==5y z3H1Xxtf(j|EEbuPUDisu!17G$#l9k|X`{y0_P`Tvm*EkchUI4#ijh;FT>+7B^fJje zq~WlTKai^`AEP@$S_uTdh(YG=inO`h?kz6kzG$A+LJ`&q^m4d6or^^0ktn_wr;uE7 zehIXTi4ba;3!mybh48edwxfr)Laf^MTC+x3wtt3?lNnF_9>}ImqKr8u@!n<`W05P1 zcU(&p7)Qx5w2Uv$W9jTv`D*;kA4^lJI(0hYYU-R#a$_xQ9_CT_w3S))J+RAzzbkW+ znA+H^k9vRjOlvQukxJnpMrA7pO;zq@N*d~WGZoGzBqQVeIm2HaGCA4@9!v1f7)xNs zbl~h5M#i{PM|8f;HEWYfXBe*lw}yLj5y`uNcO~kPDm>!C!_W3DC{L=ZD|=KoVJ^8b z=d+eybO+)#8?m-ke?(SKt>$!h%X#{+KqYdH3O{f+_lClkF zg(Bp4BoDT9jjbzFGqWx3Uu%S0Ji?1fz9H;poK#%i}9}z!Z%wIhRtBy48 zM&Iz}#bRxfs-9~%0v_^L@yJ1!=u?*<_K5fp2Eo$quXs2>pBKkpt@Nq1vVX3ym_!@9 zM}PFrYhSa&aSn|%Vs8~=3-iK?|b=_ew~5Vb@k!((KQ**R3-);=WTUfqpS_*q`$FpMJ zgO{GoLhYqLdxVPUj%4mB4~jnBjTL{?^j-6%QhQeM)`zJKn6<23IxDdxeZP;mRY}&G zE|&R4BV6YX(7mXqm8^fIL4i@LsSB=ZSdS~{F-q$dG<|;R|0iy^!jjuN z9fuTiOpKib@wxEutInZsDG{W2mT~|0H%}NKRw$v#$&=%}qm8^#D@e=bg3Ebo^I|Gh zDuXW1K(Mr9VLA8xq9;xc+|zUO`dqz4hu;n(zdRnu2RY)%ZeWhFq%J_=dzd~lRXvQp z)1ad~Oi+w5iGS8H%kQ{RK(gnQ%dm`Nx%cJUr6U=HZ>T(6X>)8h?33S{Iw{4RoSy8;cj_@IG@^&*yygi&(AhJa$S;5uv(N#1(}*2rpUZ(7VFw+Us1^|BqWW zHuZsKRbvy6j|g~QcsLOC3bQaYh}?uIiw*Mgk>?nxhZ6Vz-SE0*ms+G*>Z1+oXqCTG zgaJg#*u8i9O*dLsBho@gd64jW);qY)`5Q}l;JF&K>bsDhFkNJKm}1Y<3|;2ey|KE~ z0Q&;0?1AoU2o_uYeaZN|;j1Km>__CShv#EyzY0w7%5oJnD>J;v3cwv)aWr#3x5HbC zup=akL_9S!FvP#mgBRLvf@Si*I!!FczxcF#uZ5Twa5lk>sKAe}?741uZ(iM|Q3`BW zjE%>?cd`*H%HiU83+N$(IZf8kjkJniqIcG;CWO6g-+KO4M%z&771|6bDrzO`!xS_ ze`n?Q5ePmEB^7}=Dt4q650zJGn)p#sd&SB3+=nDTKr!!{3$=-*1xO3 z?kQk;+~5xaEz3AccI?{-HGaerl+o)06}?YOAX1FcY<{U>(dDR|+_^CMpDAu&x6+`d z8Sd_9nX=D)$E+i$PCRQiGO8NekP0f6Yc(_+95A~k#l*N&;$04QILD<*gL(tT!cx08 z7uYO*rS$%KHV`rnhUnPqxa3L^8<2Tg@nLxdKSu!T2Q zpzDfNBO!Hznx%o?Aoj@e!kTKg;UkY%9>4xKV_)3?U}?Nr{pVWj%0e;WY{hM^kTF;yw{nI`t?IC zKnIm{M`GO&J~7E&s1Hpy`dXqI@~GxCd)yeeu0}db{12|rNrZ#DD6w9qoY}K zWh+e`*H<1^kZ0EUKFL{#CTE*C`SoPLVlwQ-2=D>VD6b4x>!IZn>fbur3sOw3KmcKE z_6(eo?e$o0%v!88|G{#=A)z{wDfy@WXKqX55>8F1Xrf)pTe3}52%XPo8>h4+sB`JS zF(*l&qSUYeyLdV2fpySLtTsKYh%0P)<#3Xzaa@*Bp^9Uyw7=HKfQM5@gcja1dyr~! zJxhL79Ni-NIRrZ(t@ld|Rr_YOuFhT#{5mZEY5{KbB`o@OB4DbeBtAfjk1axqIn#-* zYU=B=v;C;S*%{ldMX8^we+6c|qFo+}Tnf{K>f%*hYX&F(3#A5v*NLM`?`P9))kw#4F z;Ifxw&27Fu(ul10gral_FOJhHJwGer>I_ifcZg-;KYyHv>AUBU$p^PZt2z%|Xv)k9 z{52)65=eqy)r;r_aI;+r02wDLW}7PHnH9>PG);J7lic@@h-nR^Hax8U-AtVjr0Pz1 zBEvlpu-lWCNi%OMKud{z3`|k9%hS!p#c!@_URn|D7O9|3XCtbI?x~M4sjZ?s2!yHL z$|IL#KdrkDGvbT6?AFAKMEahp-Bli@de#`Q3M0fdBm~UOH#ybc@cZ&{$n|Oegd%Xw zCs!5&W7cQ{=G5IzE*79#8Dq=~u&-FFP~LXwYPcCmKkU@t6yt8iqUm{gEzw7E;(sf=6818=?R##%y?%3mG+6kwXyw^R_(Ia$edrrG&<>K=r03Q*Luj!WYy=He? zU9D%+$xQoFS0fpn&dF{c?VKK8ZK8uvkrY#Y#{wKc^vZEon{t)$7N~n{gqRBbnxxzg zHE&om$1Zf1aRbrQ)7?vcSxb}K++lo6Q- zsaPs0uxPFc@IH_|@tQH=y2HM(Z}y@waHq5L4LYjCe~R45e5S&qPds>*ip+^Z?A}*q zs{LTA*Vq1ATK=j(Ki7$vPB2bEsbVvg?bSDisJeeM z#-g^o{;Xyp+g7=6Q&+Cp1-wV;Y2~_nNO{{!yWvZ8zA>mU(wFLBX|`VWK#2jmpCWC) za4^?%!80pRN~8b>OlKhJD;o=2@YexX`(93to~N|nhOTR)<~il91XqmcLWh=I8M>8m zx->PLBqhRR@j*zdHztN5zI|FbQa3$5{85q2V9H_oU4uX7?(afHw?p?XVA~J5wIdGc zFM_jLC+9ldy$+u}XU^CRL*$`<+%;#Ch)R0O3EKLg7s_$I-*n#XL87)G2wAR&@>TF( zlTWTh1?DszYv^dpVCr4jSpCkIs5|C6Q*^6f_vNcwL_Z2EUZe)J1gJ!4bWy7Z`nGA+ z+51*kMwzK@f>!C~V+45-AH-&)!vx>BX1Kk*bP0lhFt)LkA!u?*vU%!Pip*jFrPO-8 zx>HHB^#ydezT5db6V&PCF!Eui$emx^FI?Ji?O?OOgTC67!-9i`=Go0Io_h!88DjYw z5z%*UJ=Y|!p^wU8*=Kd;Gb~j^$FhMvbDFmWOM~Q-;1`Ef{A(qfDOQiR-q!1L-@ww84@e&xF6rihQZ|b~xKB?>87Xcfl;EJ$6&!GI01C1Tyst41S{%yI=o!fK*m3 zV5+NPO^(4xUO#38tl{4MH3yxjhvCV-j$sJgR(@`mXLkkhZKb|PJLJ07{=p^XJKaM> zNw!1wO`*kaMN&H(nWZ*q^juPS)U4czdObh7n#3k^Nb+c{ z=5*Y3jfHm!pfPbet*5xdP2le5qcgG9zw>)u(hJeYbeJU1Gt^H{XM4t7S${l@B;3n@ z|F))DPv(hZHv&654K15=n>ym%<6A%VrJ62yfAOsUDw^tlW_%pz2JbVWmd`41T zO=wwz6HVP~BIp}p@?o#hxJPM^A-=@1s9RlUi=vAW!3kr3VzNh!7$(8TKD7k7m3LjC z&NjrKJF`8+;km=X9mf~26RP~}0CFX0%w?1aKwBN>M+~Q@XOJOoO9>?GeXb~*sZqcz zL`naPS9GZ)CrzYz>Ud9J=*5&2;gzR2I*#$ZGPUxaE7`@z04~CoXzKgLip=y!N@;c|jHB%}hulFVYFohaRiD zZx`A`s%fu`!zh=Upxy;8IsHB%U@)JY+Z7nh9kYgJmcd0OnX8^`}x*1-$qt8tF>^Q znf0IG<3Y>QJ;L=rcjA~Yr&XBiVhXGy!tKj%;Pec4@$*k<9fjum$z*x<$)MA@R&vc0 zH~Q;w`03FX{Z4jZ){sS^PM2-0=<~_Pm$chXsc{qjRuZ|XK+34j4X1MsF9v;;IERs> zca;ZdjjBLW03r_kYT;eSGSf)jVM0h$4^jX0WG%u4QhSjpMAuHh6o;$&u!3oQ6nwNQ zwg}luMsD^uXuW@n=*Nqtjoja>y9< z$`%lo_U|8~W-qkJA4{_d$zL`q5*GH!Q3Ir_o<5%fruKw!v!dRTRivpo9_#%*TeZ(d zR0&1Bn@&9Efh6tPPB(Yv<|@q+TYA`Qp{xwc#8TJ9I)4DW<~8e=7*LhkHMBJg1vX3Z z{}3ms9DGjq5+xO_-j&j6T7Rd{$xddcs20c#K_@c+WELS`!giht6+dCHp4)n)y+d#f zSD*2+HLIk9WJR5=6uk&}c6(!Ol=!0%<-K>jd6zVib6}*)3`!{Ky4&{ZJovES_F_A& zWSeyT{Yu);?LCf?X`WFU9?bOYd zFDuGCo>2wi@HpT)48-mlgW#fZ7A7~BB{SL~CEu$-Ev+%;59Z|zW_FD35yWnya(*=y z{N?=(5J-YYOu0z*bhQ8cVp9`HUXa7wv2jb&l+&SgKHsV}lcfbd2lP|vyAL({*w#Hq z+euCmZO5M97D+RV{7t3#)^tPp0GMbKqU)u;Wg}fzi2po6q7g;2} z6eoD*ByOOK5HmBJZwZL5*$R(oU)G9!M~t6gHgy+pBfsPsI-VSy8SUWQ418p+(iPUO z_ikOi@1HsKSQ=&}AfQ?%JsL-Uctn=gfbXklE?UjpE1$I=iuNoyAOBiEQ^s0V;&4!m zU*<;F&gf6+8`PUp2D6}tbS5cS!P*Gg9_BfZ?jc@f3^oVcb>Y?YVYn$HloloD&G1CU za`taY)0nkojZG*RPVOvlsD8&(7@pQ+W++0Uv0}P^1Yr*gg*!ywsUK9 zbK%a?s{r}R{GcB!57oXa7d9|G3WeQ1dEyvU;}fxBfpvY32voN#oT3}!{#%c=uG$ke zq5^=`Oy7cHiHq%?YfDqk$rH?<8mkuL<2_ImJ0o0kotFt%#Lxe?2+;F2+us&>7~6i% zQ(-nfVpTG0a|4ulb7txGs%SL(qm$B5WRsBfx@O7N?#jhRhZhfspPrPsM_G4G&KXhB z48%?u`NkSq1sAmU7s+HK-!Xu4QmU|JXPY)sc@Y>E2W#2_E{QtcggW#&4 z47~1Qhx@x`x;2|vh@$;mF-2o)NQBJ&CjASXY8kxi3wLkl%Hrm`d&HYwf#zpthm!yu z@G!ymPWEcPP70<4u?j5>#+(3|+0rf8idvpjGtlDAA=svWqB%I~EaV8lE76oIh+0aL zU)uR2A1$mrM{qQ+6P<@&3Puyv$PG*@jaE(_5(?Sw64F3p(IJ%Iqo%zihv<&@ci2E6 z;op0BuKTH3U7mfPa%xC&fnw&^VB}D0ROsWG@}haFnRCxLzItp;Sax8uW_5RcA?CsQ zX;C#oBvO&9DPbPFXeN+20%*w_~+>Qf)MK{TX^jO|qrSUUR({~(m-!uHAB2%88- zbeaxTGMym!dEMYjmM54Q3>T{7&zAYmp#7kVXYzq5g_+)E-kG^>rQLU0JXWp6uAp@( zjg@WrY|Q-nrlFtjxsYkfXf3;MV}C|YzKP18hO1RfaSJzRT$+4ImHl~vj__H8ZQZ}0 zntzY8h|Gf(8xd(z>|>B*U?xvmtJI3?M#Er^OJh5HjT3^5hK$auF5dh31bKI^it@r- zWtfG_18g|DDgd*H;vlLzF9=NlV*xq2+|NEBFc?Pa0u2S*$F!u!`!E86Jf27e&{G!y ztlHPLWqmI(yJNNXAF$9j($nkz^4d}BX5?SVZ;AiJOr1GZsf4BvH@4ChLoXC9N?^!_8$JVwX zQm+(p>4ESwjmWgsN2Gq`ym-7>JjF&q=_0>_{F?nECweTsO6MY3suY|}@eV)2O>EkN z7k^=w>NE&dP5nVI8=@UD@}`hT(s<~s+?Ar-SjyLFz}FZ3>iy#Z!psY4XP)lKBGIq{ z&h@K|Bf@XVp)YmjXx-hlZA^=TsXTXQHxID;KqFS$+LCfJewb7#Ec(D;Pt}Fu17t;G z*PJzbXU6yf#|67adS<=c&Y%Muc5c1{$HA4wo+@;X^~fL7!|%c+U)?mK)c6xLZ1Sjb zUF6EK0A+<)kQ1r%=hf{+)$W-c$J+OsI$+Nhl>j6rhQ4|wdd5M1Ocl*hV!|BRyNuG` zU=3Vi!mUbODdPWMFZN>DfMb&P<`B2l!(;7Y+Dn2gWCH9O;oX%4biS!agPApvtJlX@L4La!15x6!&GB8k~fKPB`J^8Mk5q1jsNF1hRT-Kd-^dF$>%}GiPybz_8 zCo&Vd|9mpX8y!$HjcL@C)NIHAl1>Jl2$AAo+w(`@*YQ5QXBPl^G#n!VxBBvBT? zq6kxT%lKy02#*lp?zi!Olhx5=KA`x%pf zZu6UXKWjg^Yz1aEk_;a2A9=w5xd&-(4dUrod+25^c4eI6V%>iR**M#dY)@1cY^OAx zcb)uz$yB@&QgTw&{Pe)ev6jeP4+Gl?Rxq2F5$Yu3KX>}^m)--ts14!I-yTY&T2KQ9 zT#jN*+36yUsP$-&72AX!X&|IG3x=Ql7xH%P%v5PB9L{$>j!n*tHF|Hft_5}rwbTUV z?rw@33&sF>{SROnz6uo)G`kM0gXkUF;FQ&Dh$=%Y{8zc(vhVkc z(a7-#{Gt^Z!zl#h4BMO(qJ;89B0Tm+t$j5>^crYV`qjsw=TvCa#@GHWp-CP<8l?Y) z#2vZB8G*&?1N-S2ae04Zjn!kpxAdt#rN*D~s>gT>5CvB7lhor1x6Gz~*En4%N}%}8 zVfRoa9Olwi-xdX~*t+Wr&`OH3B@&hNK*;`;&K$2}+i!`H!OPQF0J(?`Z|HE{1vX54 z?rce!qwom~sp8wrDi=A~AIejNv@_QL&%dxN=U@$uD(fVKKwD{8?=P5-J$ zih|-n08gC!0~#Bqb?31h!LKY-`1MVr&$p%xIK+Vy69D7G>T)tJ zHTHlO-EpS01kWgobd;)nAOtIDGxET zDPT;{$rLouJC1uAJj_eAxNl|vWdVGhK|xmT!`A@x|8{}q$`#Meyzz@8C(_NUs|E&8 zYM|#uLMZ9|Hcx=TPy0Z4KYa{vGU literal 0 HcmV?d00001 diff --git a/helpmehelpapplication/src/main/resources/images/img.png b/helpmehelpapplication/src/main/resources/images/img.png new file mode 100644 index 0000000000000000000000000000000000000000..586512b98f11d4c8321e05e10d38db74d08df18b GIT binary patch literal 886 zcmeAS@N?(olHy`uVBq!ia0vp^0YDtX!3HFMUN*YMz`$(l>Eakt5%+fP-NMBNB5sHSw+gCDSiA~2ckep1;fECO18$cE?`K=;A3Rv_ z%*&~3ElXo^;M48LH*}oV=wEHn|6eF@T8&#}(URmpUp>?lC;qu|=v=P_x8{G__lr+m zZ{N^!zbYlNqAqx8)r03BjaJnJCNAIUx8Tz*&)KUk*T(kG$b3=!q4>Sss+v09_p7a> zuLe)6dH#O$?tsac7r%Tj$GCaJ?TNcC$F#fMh`iBzV(ZaDGwn&gEO!0)(0!yZYodNv z{PJMKtIOnzz8#xM1&JtzGsj=1Tr6$Cbw7{N-0lkRP!f~hWrU3KFltngn+n7Srz7_7Q6x@*fYkye6%8YW}Gako^ z^PZYsWV&A+$5FK-YoG6hp5xB9cNNsD&A7%ZEP7J?`!%*u3!b-8hxW#@hGsl}b@RMf z$ilg0&kUy+9PT*tQnoU8;d3Xg@H(M*Me)U}*K^MLRKI1JWmKa<_^Ouh5^ z--Y|i-#uRU<44BjytgLquN;rgm+gBImwx}nZSl_T6R*>**Cv%&eEc1&67}fcze(z{ zxy#$aQoeuKu=f4KnytI`q`i1qbNa7R@0Q8w65c73eXXK*p5!|kk}Rjc&HrDFMR4i1 z3r&l=YZpffNJR0JYMS(yIjZmqU5OE|-4?C&ELr*2qy0`#mL2_g!FSEoE#<1~^xd8? zbql8z7*s!gA0p#_cMre1XK382w{yknfBxLdb(Y0LO+4lB?kwrq+tx3sXEh~uWgx|7vl=$Hq(mi1gFh4PPy85}Sb4q9e08zV{egFUf literal 0 HcmV?d00001 From a97a0a561ed973efb6a615117760b5ecfdb6f761 Mon Sep 17 00:00:00 2001 From: Robin Strand Prestmo Date: Tue, 10 Mar 2026 14:05:57 +0100 Subject: [PATCH 034/123] Added config to Main to run JavaFX.forntPage, and to pom.xml --- .../java/ntnu/{sytemutvikling => systemutvikling}/team6/Main.java | 0 .../{sytemutvikling => systemutvikling}/team6/models/Charity.java | 0 .../team6/models/CharityRegistry.java | 0 .../team6/models/Donation.java | 0 .../team6/models/DonationRegistry.java | 0 .../team6/models/Feedback.java | 0 .../team6/models/UserRegistry.java | 0 .../team6/models/user/Inbox.java | 0 .../team6/models/user/Language.java | 0 .../team6/models/user/Message.java | 0 .../team6/models/user/Role.java | 0 .../team6/models/user/Settings.java | 0 .../team6/models/user/User.java | 0 .../team6/security/PasswordHasher.java | 0 .../team6/service/AuthenticationService.java | 0 .../team6/service/CharityService.java | 0 .../team6/service/DonationService.java | 0 .../team6/service/FeedbackService.java | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/Main.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/models/Charity.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/models/CharityRegistry.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/models/Donation.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/models/DonationRegistry.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/models/Feedback.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/models/UserRegistry.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/models/user/Inbox.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/models/user/Language.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/models/user/Message.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/models/user/Role.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/models/user/Settings.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/models/user/User.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/security/PasswordHasher.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/service/AuthenticationService.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/service/CharityService.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/service/DonationService.java (100%) rename helpmehelpapplication/src/main/java/ntnu/{sytemutvikling => systemutvikling}/team6/service/FeedbackService.java (100%) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/Main.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/Main.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/Main.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/Main.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Charity.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Charity.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Charity.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Charity.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/CharityRegistry.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/CharityRegistry.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/CharityRegistry.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/CharityRegistry.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Donation.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Donation.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Donation.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Donation.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DonationRegistry.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/DonationRegistry.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/DonationRegistry.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/DonationRegistry.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Feedback.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Feedback.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Feedback.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Feedback.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/UserRegistry.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/UserRegistry.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/UserRegistry.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/UserRegistry.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/user/Inbox.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Inbox.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/user/Inbox.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Inbox.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/user/Language.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Language.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/user/Language.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Language.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/user/Message.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Message.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/user/Message.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Message.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/user/Role.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Role.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/user/Role.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Role.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/user/Settings.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Settings.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/user/Settings.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Settings.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/user/User.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/User.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/user/User.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/User.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/security/PasswordHasher.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/security/PasswordHasher.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/security/PasswordHasher.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/security/PasswordHasher.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/AuthenticationService.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/AuthenticationService.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/AuthenticationService.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/AuthenticationService.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/CharityService.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/CharityService.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/CharityService.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/CharityService.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/DonationService.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/DonationService.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/DonationService.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/DonationService.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/FeedbackService.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/FeedbackService.java similarity index 100% rename from helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/service/FeedbackService.java rename to helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/FeedbackService.java From b24af6cedd04c3cca72763a322b7bf2d5b0a482a Mon Sep 17 00:00:00 2001 From: Robin Strand Prestmo Date: Tue, 10 Mar 2026 14:43:47 +0100 Subject: [PATCH 035/123] Added config to pom.xml and main. Also added fxml files for JavaFX --- helpmehelpapplication/pom.xml | 7 ++- .../java/ntnu/systemutvikling/team6/Main.java | 21 +++++++-- .../systemutvikling/team6/models/Charity.java | 2 +- .../team6/models/CharityRegistry.java | 2 +- .../team6/models/Donation.java | 4 +- .../team6/models/DonationRegistry.java | 2 +- .../team6/models/Feedback.java | 4 +- .../team6/models/UserRegistry.java | 2 +- .../team6/models/user/Inbox.java | 2 +- .../team6/models/user/Language.java | 2 +- .../team6/models/user/Message.java | 2 +- .../team6/models/user/Role.java | 2 +- .../team6/models/user/Settings.java | 2 +- .../team6/models/user/User.java | 4 +- .../team6/security/PasswordHasher.java | 2 +- .../team6/service/CharityService.java | 2 +- .../team6/service/DonationService.java | 2 +- .../team6/service/FeedbackService.java | 2 +- .../src/main/resources/fxml/donationPage.fxml | 7 +-- .../src/main/resources/fxml/frontPage.fxml | 4 +- .../team6/models/CharityRegistryTest.java | 2 - .../team6/models/CharityTest.java | 1 - .../team6/models/DonationTest.java | 10 ++--- .../team6/models/FeedbackTest.java | 9 ++-- .../team6/models/user/SettingsTest.java | 2 - .../team6/models/user/UserTest.java | 43 +++++-------------- .../team6/security/PasswordHasherTest.java | 1 - 27 files changed, 67 insertions(+), 78 deletions(-) diff --git a/helpmehelpapplication/pom.xml b/helpmehelpapplication/pom.xml index da3d4e2..16743a5 100644 --- a/helpmehelpapplication/pom.xml +++ b/helpmehelpapplication/pom.xml @@ -25,6 +25,11 @@ javafx-controls 25.0.1 + + org.openjfx + javafx-fxml + 25.0.1 + @@ -49,7 +54,7 @@ javafx-maven-plugin 0.0.8 - ntnu.gruppe21.Main + ntnu.systemutvikling.team6.Main diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/Main.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/Main.java index b30c2e3..3cc6b5d 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/Main.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/Main.java @@ -1,7 +1,22 @@ -package ntnu.sytemutvikling.team6; +package ntnu.systemutvikling.team6; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.stage.Stage; + +public class Main extends Application { + + @Override + public void start(Stage stage) throws Exception { + FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/fxml/frontPage.fxml")); + Scene scene = new Scene(fxmlLoader.load()); + stage.setTitle("Help Me Help"); + stage.setScene(scene); + stage.show(); + } -public class Main { public static void main(String[] args) { - System.out.println("Hello world!"); + launch(args); } } diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Charity.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Charity.java index 2c2cb3f..45c5c26 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Charity.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Charity.java @@ -4,7 +4,7 @@ * * @author Adrian Balunan */ -package ntnu.sytemutvikling.team6.models; +package ntnu.systemutvikling.team6.models; import java.util.ArrayList; import java.util.List; diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/CharityRegistry.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/CharityRegistry.java index 2935c74..4a270fc 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/CharityRegistry.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/CharityRegistry.java @@ -1,4 +1,4 @@ -package ntnu.sytemutvikling.team6.models; +package ntnu.systemutvikling.team6.models; import java.util.*; diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Donation.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Donation.java index 70f6268..4351a1b 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Donation.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Donation.java @@ -1,8 +1,8 @@ -package ntnu.sytemutvikling.team6.models; +package ntnu.systemutvikling.team6.models; import java.time.LocalDateTime; import java.util.UUID; -import ntnu.sytemutvikling.team6.models.user.User; +import ntnu.systemutvikling.team6.models.user.User; public class Donation { /* UUID for uniquely identifying each donation */ diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/DonationRegistry.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/DonationRegistry.java index ff6d4e2..b06009d 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/DonationRegistry.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/DonationRegistry.java @@ -1,4 +1,4 @@ -package ntnu.sytemutvikling.team6.models; +package ntnu.systemutvikling.team6.models; import java.util.*; diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Feedback.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Feedback.java index f753e8a..490c47e 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Feedback.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/Feedback.java @@ -1,8 +1,8 @@ -package ntnu.sytemutvikling.team6.models; +package ntnu.systemutvikling.team6.models; import java.time.LocalDateTime; import java.util.UUID; -import ntnu.sytemutvikling.team6.models.user.User; +import ntnu.systemutvikling.team6.models.user.User; public class Feedback { /* Feedback id */ diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/UserRegistry.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/UserRegistry.java index c858e31..514cbec 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/UserRegistry.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/UserRegistry.java @@ -1,3 +1,3 @@ -package ntnu.sytemutvikling.team6.models; +package ntnu.systemutvikling.team6.models; public class UserRegistry {} diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Inbox.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Inbox.java index b560092..e0d95a9 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Inbox.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Inbox.java @@ -1,4 +1,4 @@ -package ntnu.sytemutvikling.team6.models.user; +package ntnu.systemutvikling.team6.models.user; import java.util.*; diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Language.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Language.java index ddc5d82..c568ede 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Language.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Language.java @@ -1,4 +1,4 @@ -package ntnu.sytemutvikling.team6.models.user; +package ntnu.systemutvikling.team6.models.user; /** * Supported application languages. diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Message.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Message.java index ef9707b..b70ece1 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Message.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Message.java @@ -1,4 +1,4 @@ -package ntnu.sytemutvikling.team6.models.user; +package ntnu.systemutvikling.team6.models.user; import java.time.LocalDateTime; import java.util.UUID; diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Role.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Role.java index 74e134f..94e0b22 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Role.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Role.java @@ -1,4 +1,4 @@ -package ntnu.sytemutvikling.team6.models.user; +package ntnu.systemutvikling.team6.models.user; /** * Available users diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Settings.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Settings.java index dd76557..d814283 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Settings.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/Settings.java @@ -1,4 +1,4 @@ -package ntnu.sytemutvikling.team6.models.user; +package ntnu.systemutvikling.team6.models.user; // Mangler Enhetstesting diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/User.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/User.java index 64594a0..ef59000 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/User.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/models/user/User.java @@ -1,7 +1,7 @@ -package ntnu.sytemutvikling.team6.models.user; +package ntnu.systemutvikling.team6.models.user; import java.util.UUID; -import ntnu.sytemutvikling.team6.security.PasswordHasher; +import ntnu.systemutvikling.team6.security.PasswordHasher; /** * Represents a user in the system. diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/security/PasswordHasher.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/security/PasswordHasher.java index 8c2f7d9..baf5e4b 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/security/PasswordHasher.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/security/PasswordHasher.java @@ -1,4 +1,4 @@ -package ntnu.sytemutvikling.team6.security; +package ntnu.systemutvikling.team6.security; import java.security.MessageDigest; import java.security.SecureRandom; diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/CharityService.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/CharityService.java index 3b66851..b471c19 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/CharityService.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/CharityService.java @@ -1,3 +1,3 @@ -package ntnu.sytemutvikling.team6.service; +package ntnu.systemutvikling.team6.service; public class CharityService {} diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/DonationService.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/DonationService.java index 17692d5..fca9633 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/DonationService.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/DonationService.java @@ -1,3 +1,3 @@ -package ntnu.sytemutvikling.team6.service; +package ntnu.systemutvikling.team6.service; public class DonationService {} diff --git a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/FeedbackService.java b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/FeedbackService.java index 27eee23..ae1f984 100644 --- a/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/FeedbackService.java +++ b/helpmehelpapplication/src/main/java/ntnu/systemutvikling/team6/service/FeedbackService.java @@ -1,3 +1,3 @@ -package ntnu.sytemutvikling.team6.service; +package ntnu.systemutvikling.team6.service; public class FeedbackService {} diff --git a/helpmehelpapplication/src/main/resources/fxml/donationPage.fxml b/helpmehelpapplication/src/main/resources/fxml/donationPage.fxml index d5f8881..a42224b 100644 --- a/helpmehelpapplication/src/main/resources/fxml/donationPage.fxml +++ b/helpmehelpapplication/src/main/resources/fxml/donationPage.fxml @@ -17,7 +17,7 @@ - + @@ -41,7 +41,7 @@ - + @@ -99,6 +99,7 @@ + @@ -229,7 +230,7 @@ - +

+ * Summary of function: + *

    + *
  • Inserts new records
  • + *
  • Updates existing records using {@code ON DUPLICATE KEY UPDATE}
  • + *
  • Removes database records that are not present in the provided list
  • + *
+ * A temporary table is used to determine which records should be deleted. + *

+ * + * @param charities a list of ApiCharityDaya objects + * @throws RuntimeException if a {@link SQLException} occurs during database synchronization + */ + + public void updateCharities(List charities) { + Connection conn = null; + try { + conn = connection.getMySqlConnection(); + conn.setAutoCommit(false); + // Inserts objects values into charities + String sql_query = """ + INSERT INTO charities (org_number, name, status, url, is_pre_approved) + VALUES (?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + name = VALUES(name), + status = VALUES(status), + url = VALUES(url), + is_pre_approved = VALUES(is_pre_approved) + """; + + try (PreparedStatement s = conn.prepareStatement(sql_query)) { + for (APICharityData charity : charities) { + s.setString(1, charity.getOrg_number().replaceAll("\\s", "")); + s.setString(2, charity.getName()); + s.setString(3, charity.getStatus()); + s.setString(4, charity.getUrl()); + s.setBoolean(5, charity.getIs_pre_approved()); + + s.addBatch(); + } + s.executeBatch(); + } + + + // Temporary table for parity check + sql_query = "CREATE TEMPORARY TABLE temp (org_number VARCHAR(100) PRIMARY KEY)"; + try (PreparedStatement temp_create = conn.prepareStatement(sql_query)) { + temp_create.execute(); + } + + String sql_update_temp_table = "INSERT INTO temp (org_number) VALUES (?)"; + try (PreparedStatement temp_update = conn.prepareStatement(sql_update_temp_table)) { + for (APICharityData charity : charities) { + temp_update.setString(1, charity.getOrg_number().replaceAll("\\s", "")); + temp_update.addBatch(); + } + temp_update.executeBatch(); + } + + + + // Removes records not found in the list from charities table + sql_query = """ + DELETE FROM charities c + WHERE NOT EXISTS ( + SELECT 1 + FROM temp t + WHERE c.org_number = t.org_number + ) + """; + + try (PreparedStatement delete_old_records = conn.prepareStatement(sql_query)){ + delete_old_records.execute(); + } + + conn.commit(); + } catch (SQLException e) { + + if (conn != null) { + try { + conn.rollback(); + } catch (SQLException ex) { + ex.printStackTrace(); + } + } + + e.printStackTrace(); + throw new RuntimeException("ERROR: Something went wrong during updating charities table."); + } finally { + if (conn != null) { + try { + conn.setAutoCommit(true); + conn.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + } } From 8bce5decff560a6e81fba9cce749f7aa515b3fcf Mon Sep 17 00:00:00 2001 From: AdrianBalunan Date: Wed, 11 Mar 2026 15:10:17 +0100 Subject: [PATCH 044/123] Feat: Seperate File for Database Connection --- .../team6/database/DatabaseConnection.java | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/database/DatabaseConnection.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/database/DatabaseConnection.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/database/DatabaseConnection.java new file mode 100644 index 0000000..437bbf9 --- /dev/null +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/database/DatabaseConnection.java @@ -0,0 +1,96 @@ +package ntnu.sytemutvikling.team6.database; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +/** + * Represents a reusable database connection through enviroment variables. + */ + +public class DatabaseConnection { + private final String databaseURL; + private final String username; + private final String password; + /** + * Constructs a new {@code DatabaseConnection} using database credentials + * retrieved from system environment variables. + *

+ * Required environment variables: + *

    + *
  • {@code HMH_DB_URL}
  • + *
  • {@code HMH_DB_USERNAME}
  • + *
  • {@code HMH_DB_PASSWORD}
  • + *
+ *

+ * + * @throws IllegalStateException if either databaseURL, username, or password is {@code null} or blank + */ + + // Values stored in system environment variables for security (using ntnu's phpmyadmin for this project) + public DatabaseConnection() { + this.databaseURL = System.getenv("HMH_DB_URL"); + this.username = System.getenv("HMH_DB_USERNAME"); + this.password = System.getenv("HMH_DB_PASSWORD"); + + if (this.databaseURL == null || this.databaseURL.isBlank()) { + throw new IllegalStateException("Database environment variable URL has not been set"); + } + + if (this.username == null || this.username.isBlank()) { + throw new IllegalStateException("Username environment variable has not been set"); + } + + if (this.password == null || this.password.isBlank()) { + throw new IllegalStateException("Password environment variable has not been set"); + } + } + + /** + * Constructs a new {@code DatabaseConnection} using database credentials + *

+ * Used primarily for JUnit tests. Production should use the constructor + * using environment variables. + *

+ * + * @param databaseURL the url to the database + * @param username the username used to log in to the database + * @param password the password used to log in to the database + * @throws IllegalArgumentException if databaseURL, username, or password is + * {@code null} or blank + */ + + public DatabaseConnection(String databaseURL, String username, String password) { + this.databaseURL = databaseURL; + this.username = username; + this.password = password; + + if (this.databaseURL == null || this.databaseURL.isBlank()) { + throw new IllegalArgumentException("Database environment variable URL has not been set"); + } + + if (this.username == null || this.username.isBlank()) { + throw new IllegalArgumentException("Username environment variable has not been set"); + } + + if (this.password == null || this.password.isBlank()) { + throw new IllegalArgumentException("Password environment variable has not been set"); + } + } + + + /** + * Creates and returns a new MySQL database connection. + * + * @return a {@link Connection} to the database + * @throws SQLException if the connection cannot be established + */ + public Connection getMySqlConnection() throws SQLException { + return DriverManager.getConnection(databaseURL, username, password); + } + + /** + * + */ + +} From 609ca49450a1511cab01a51ef978252716c564df Mon Sep 17 00:00:00 2001 From: AdrianBalunan Date: Wed, 11 Mar 2026 15:10:43 +0100 Subject: [PATCH 045/123] Feat: Manager can only create tables if needed (for now) --- .../team6/database/DatabaseManager.java | 181 +----------------- 1 file changed, 10 insertions(+), 171 deletions(-) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/database/DatabaseManager.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/database/DatabaseManager.java index 4968159..3e5357d 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/database/DatabaseManager.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/database/DatabaseManager.java @@ -1,87 +1,27 @@ package ntnu.sytemutvikling.team6.database; -import ntnu.sytemutvikling.team6.DAO.APICharityData; +import ntnu.sytemutvikling.team6.scraper.APICharityData; import java.sql.*; import java.util.List; /** - * Manages creation and synchronization of database tables in a MySQL database. + * Manages creation of MySQL tables (for now) *

- * This class is responsible for establishing connections using environment variables + * This class is responsible for creating the tables needed for the application, if not done already * and maintaining the {@code charities} table based on data retrieved from the IK API. *

*/ public class DatabaseManager { - private final String databaseURL; - private final String username; - private final String password; + private final DatabaseConnection connection; /** - Constructs a new {@code DatabaseManager} using database credentials - * retrieved from system environment variables. - *

- * Required environment variables: - *

    - *
  • {@code HMH_DB_URL}
  • - *
  • {@code HMH_DB_USERNAME}
  • - *
  • {@code HMH_DB_PASSWORD}
  • - *
- *

- * - * @throws IllegalStateException if either databaseURL, username, or password is {@code null} or blank - */ - - // Values stored in system environment variables for security (using ntnu's phpmyadmin for this project) - public DatabaseManager() { - this.databaseURL = System.getenv("HMH_DB_URL"); - this.username = System.getenv("HMH_DB_USERNAME"); - this.password = System.getenv("HMH_DB_PASSWORD"); - - if (this.databaseURL == null || this.databaseURL.isBlank()) { - throw new IllegalStateException("Database environment variable URL has not been set"); - } - - if (this.username == null || this.username.isBlank()) { - throw new IllegalStateException("Username environment variable has not been set"); - } - - if (this.password == null || this.password.isBlank()) { - throw new IllegalStateException("Password environment variable has not been set"); - } - } - - /** - * Constructs a new {@code DatabaseManager} using database credentials - *

- * Used primarily for JUnit tests. Production should use the constructor - * using environment variables. - *

- * - * @param databaseURL the url to the database - * @param username the username used to log in to the database - * @param password the password used to log in to the database - * @throws IllegalArgumentException if databaseURL, username, or password is - * {@code null} or blank + * Contractor for DatabaseManager. It uses a DatabaseConnection object that contains a connection credentials + * using environment variables in the DatabaseConnection. */ - - public DatabaseManager(String databaseURL, String username, String password) { - this.databaseURL = databaseURL; - this.username = username; - this.password = password; - - if (this.databaseURL == null || this.databaseURL.isBlank()) { - throw new IllegalArgumentException("Database environment variable URL has not been set"); - } - - if (this.username == null || this.username.isBlank()) { - throw new IllegalArgumentException("Username environment variable has not been set"); - } - - if (this.password == null || this.password.isBlank()) { - throw new IllegalArgumentException("Password environment variable has not been set"); - } + public DatabaseManager(){ + this.connection = new DatabaseConnection(); } /** @@ -92,7 +32,7 @@ public DatabaseManager(String databaseURL, String username, String password) { * @throws RuntimeException if a {@link SQLException} occurs while creating the table */ - public void createCharitiesTable() { + public void createTables() { String sql_query = """ -- MySQL Workbench Forward Engineering @@ -148,7 +88,7 @@ FOREIGN KEY (`Charities_UUID`) """; - try (Connection conn = DriverManager.getConnection(databaseURL, username, password); + try (Connection conn = connection.getMySqlConnection(); Statement s = conn.createStatement()) { s.execute(sql_query); @@ -158,106 +98,5 @@ FOREIGN KEY (`Charities_UUID`) } } - /** - * Synchronizes the {@code charities} table with the provided list of APICharityData. - *

- * Summary of function: - *

    - *
  • Inserts new records
  • - *
  • Updates existing records using {@code ON DUPLICATE KEY UPDATE}
  • - *
  • Removes database records that are not present in the provided list
  • - *
- * A temporary table is used to determine which records should be deleted. - *

- * - * @param charities a list of ApiCharityDaya objects - * @throws RuntimeException if a {@link SQLException} occurs during database synchronization - */ - - public void updateCharities(List charities) { - Connection conn = null; - try { - conn = DriverManager.getConnection(databaseURL, username, password); - conn.setAutoCommit(false); - // Inserts objects values into charities - String sql_query = """ - INSERT INTO charities (org_number, name, status, url, is_pre_approved) - VALUES (?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - name = VALUES(name), - status = VALUES(status), - url = VALUES(url), - is_pre_approved = VALUES(is_pre_approved) - """; - - try (PreparedStatement s = conn.prepareStatement(sql_query)) { - for (APICharityData charity : charities) { - s.setString(1, charity.getOrg_number().replaceAll("\\s", "")); - s.setString(2, charity.getName()); - s.setString(3, charity.getStatus()); - s.setString(4, charity.getUrl()); - s.setBoolean(5, charity.getIs_pre_approved()); - - s.addBatch(); - } - s.executeBatch(); - } - - - // Temporary table for parity check - sql_query = "CREATE TEMPORARY TABLE temp (org_number VARCHAR(100) PRIMARY KEY)"; - try (PreparedStatement temp_create = conn.prepareStatement(sql_query)) { - temp_create.execute(); - } - - String sql_update_temp_table = "INSERT INTO temp (org_number) VALUES (?)"; - try (PreparedStatement temp_update = conn.prepareStatement(sql_update_temp_table)) { - for (APICharityData charity : charities) { - temp_update.setString(1, charity.getOrg_number().replaceAll("\\s", "")); - temp_update.addBatch(); - } - temp_update.executeBatch(); - } - - - // Removes records not found in the list from charities table - sql_query = """ - DELETE FROM charities c - WHERE NOT EXISTS ( - SELECT 1 - FROM temp t - WHERE c.org_number = t.org_number - ) - """; - - try (PreparedStatement delete_old_records = conn.prepareStatement(sql_query)){ - delete_old_records.execute(); - } - - conn.commit(); - } catch (SQLException e) { - - if (conn != null) { - try { - conn.rollback(); - } catch (SQLException ex) { - ex.printStackTrace(); - } - } - - e.printStackTrace(); - throw new RuntimeException("ERROR: Something went wrong during updating charities table."); - } finally { - if (conn != null) { - try { - conn.setAutoCommit(true); - conn.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - } - - } } \ No newline at end of file From 3f55f66e65c36ad5655d2118995011c65587d823 Mon Sep 17 00:00:00 2001 From: AdrianBalunan Date: Wed, 11 Mar 2026 15:11:22 +0100 Subject: [PATCH 046/123] Feat: DAO data acsess object(s) will help SETTING new data to database --- .../sytemutvikling/team6/DAO/CharityDAO.java | 273 ++++++++++++++++++ .../sytemutvikling/team6/DAO/DonationDAO.java | 4 + 2 files changed, 277 insertions(+) create mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/DAO/CharityDAO.java create mode 100644 helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/DAO/DonationDAO.java diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/DAO/CharityDAO.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/DAO/CharityDAO.java new file mode 100644 index 0000000..810eca1 --- /dev/null +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/DAO/CharityDAO.java @@ -0,0 +1,273 @@ +package ntnu.sytemutvikling.team6.DAO; + +import ntnu.sytemutvikling.team6.scraper.APICharityData; + +import java.sql.*; +import java.util.List; + +/** + * Manages the charity + *

+ * This class is responsible for establishing connections using environment variables + * and maintaining the {@code charities} table based on data retrieved from the IK API. + *

+ */ + +public class CharityDAO { + +import APICharityData; + +import java.sql.*; +import java.util.List; + + + + public class DatabaseManager { + private final String databaseURL; + private final String username; + private final String password; + + /** + Constructs a new {@code DatabaseManager} using database credentials + * retrieved from system environment variables. + *

+ * Required environment variables: + *

    + *
  • {@code HMH_DB_URL}
  • + *
  • {@code HMH_DB_USERNAME}
  • + *
  • {@code HMH_DB_PASSWORD}
  • + *
+ *

+ * + * @throws IllegalStateException if either databaseURL, username, or password is {@code null} or blank + */ + + // Values stored in system environment variables for security (using ntnu's phpmyadmin for this project) + public DatabaseManager() { + this.databaseURL = System.getenv("HMH_DB_URL"); + this.username = System.getenv("HMH_DB_USERNAME"); + this.password = System.getenv("HMH_DB_PASSWORD"); + + if (this.databaseURL == null || this.databaseURL.isBlank()) { + throw new IllegalStateException("Database environment variable URL has not been set"); + } + + if (this.username == null || this.username.isBlank()) { + throw new IllegalStateException("Username environment variable has not been set"); + } + + if (this.password == null || this.password.isBlank()) { + throw new IllegalStateException("Password environment variable has not been set"); + } + } + + /** + * Constructs a new {@code DatabaseManager} using database credentials + *

+ * Used primarily for JUnit tests. Production should use the constructor + * using environment variables. + *

+ * + * @param databaseURL the url to the database + * @param username the username used to log in to the database + * @param password the password used to log in to the database + * @throws IllegalArgumentException if databaseURL, username, or password is + * {@code null} or blank + */ + + public DatabaseManager(String databaseURL, String username, String password) { + this.databaseURL = databaseURL; + this.username = username; + this.password = password; + + if (this.databaseURL == null || this.databaseURL.isBlank()) { + throw new IllegalArgumentException("Database environment variable URL has not been set"); + } + + if (this.username == null || this.username.isBlank()) { + throw new IllegalArgumentException("Username environment variable has not been set"); + } + + if (this.password == null || this.password.isBlank()) { + throw new IllegalArgumentException("Password environment variable has not been set"); + } + } + + /** + * Creates the {@code charities} table if it does not already exist. + *

+ * The table structure is based on fields from {@link APICharityData}. + *

+ * @throws RuntimeException if a {@link SQLException} occurs while creating the table + */ + + public void createTables() { + String sql_query = """ + -- MySQL Workbench Forward Engineering + + SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0; + SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0; + SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'; + + -- ----------------------------------------------------- + -- Schema HelpMeHelp + -- ----------------------------------------------------- + + -- ----------------------------------------------------- + -- Schema HelpMeHelp + -- ----------------------------------------------------- + --CREATE SCHEMA IF NOT EXISTS `HelpMeHelp` DEFAULT CHARACTER SET utf8 ; + --USE `HelpMeHelp` ; + USE apbaluna; + + -- ----------------------------------------------------- + -- Table `HelpMeHelp`.`Charities` + -- ----------------------------------------------------- + CREATE TABLE IF NOT EXISTS `HelpMeHelp`.`Charities` ( + `UUID` CHAR(36) NOT NULL, + `charity_name` VARCHAR(255) NOT NULL, + `charity_description` VARCHAR(255) NOT NULL, + `isVerified` TINYINT NOT NULL, + `category` VARCHAR(45) NOT NULL, + PRIMARY KEY (`UUID`)) + ENGINE = InnoDB; + + + -- ----------------------------------------------------- + -- Table `HelpMeHelp`.`Donations` + -- ----------------------------------------------------- + CREATE TABLE IF NOT EXISTS `HelpMeHelp`.`Donations` ( + `UUID` CHAR(36) NOT NULL, + `amount` DECIMAL NOT NULL, + `date` DATE NOT NULL, + `Charities_UUID` CHAR(36) NOT NULL, + PRIMARY KEY (`UUID`), + INDEX `fk_Donations_Charities_idx` (`Charities_UUID` ASC) VISIBLE, + CONSTRAINT `fk_Donations_Charities` + FOREIGN KEY (`Charities_UUID`) + REFERENCES `HelpMeHelp`.`Charities` (`UUID`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) + ENGINE = InnoDB; + + + SET SQL_MODE=@OLD_SQL_MODE; + SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; + SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS; + + """; + + try (Connection conn = DriverManager.getConnection(databaseURL, username, password); + Statement s = conn.createStatement()) { + + s.execute(sql_query); + } catch (SQLException e) { + e.printStackTrace(); + throw new RuntimeException("Error creating table."); + } + } + + /** + * Synchronizes the {@code charities} table with the provided list of APICharityData. + *

+ * Summary of function: + *

    + *
  • Inserts new records
  • + *
  • Updates existing records using {@code ON DUPLICATE KEY UPDATE}
  • + *
  • Removes database records that are not present in the provided list
  • + *
+ * A temporary table is used to determine which records should be deleted. + *

+ * + * @param charities a list of ApiCharityDaya objects + * @throws RuntimeException if a {@link SQLException} occurs during database synchronization + */ + + public void updateCharities(List charities) { + Connection conn = null; + try { + conn = DriverManager.getConnection(databaseURL, username, password); + conn.setAutoCommit(false); + // Inserts objects values into charities + String sql_query = """ + INSERT INTO charities (org_number, name, status, url, is_pre_approved) + VALUES (?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + name = VALUES(name), + status = VALUES(status), + url = VALUES(url), + is_pre_approved = VALUES(is_pre_approved) + """; + + try (PreparedStatement s = conn.prepareStatement(sql_query)) { + for (APICharityData charity : charities) { + s.setString(1, charity.getOrg_number().replaceAll("\\s", "")); + s.setString(2, charity.getName()); + s.setString(3, charity.getStatus()); + s.setString(4, charity.getUrl()); + s.setBoolean(5, charity.getIs_pre_approved()); + + s.addBatch(); + } + s.executeBatch(); + } + + + // Temporary table for parity check + sql_query = "CREATE TEMPORARY TABLE temp (org_number VARCHAR(100) PRIMARY KEY)"; + try (PreparedStatement temp_create = conn.prepareStatement(sql_query)) { + temp_create.execute(); + } + + String sql_update_temp_table = "INSERT INTO temp (org_number) VALUES (?)"; + try (PreparedStatement temp_update = conn.prepareStatement(sql_update_temp_table)) { + for (APICharityData charity : charities) { + temp_update.setString(1, charity.getOrg_number().replaceAll("\\s", "")); + temp_update.addBatch(); + } + temp_update.executeBatch(); + } + + + + // Removes records not found in the list from charities table + sql_query = """ + DELETE FROM charities c + WHERE NOT EXISTS ( + SELECT 1 + FROM temp t + WHERE c.org_number = t.org_number + ) + """; + + try (PreparedStatement delete_old_records = conn.prepareStatement(sql_query)){ + delete_old_records.execute(); + } + + conn.commit(); + } catch (SQLException e) { + + if (conn != null) { + try { + conn.rollback(); + } catch (SQLException ex) { + ex.printStackTrace(); + } + } + + e.printStackTrace(); + throw new RuntimeException("ERROR: Something went wrong during updating charities table."); + } finally { + if (conn != null) { + try { + conn.setAutoCommit(true); + conn.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + } + } +} diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/DAO/DonationDAO.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/DAO/DonationDAO.java new file mode 100644 index 0000000..b902276 --- /dev/null +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/DAO/DonationDAO.java @@ -0,0 +1,4 @@ +package ntnu.sytemutvikling.team6.DAO; + +public class DonationDAO { +} From f4fc2ba6d78b6641a376b1a780d7bf8f84beeeb8 Mon Sep 17 00:00:00 2001 From: AdrianBalunan Date: Wed, 11 Mar 2026 15:48:29 +0100 Subject: [PATCH 047/123] Fix/Feat: Charity class looks very similar to APICharityData, will keep APICharityData though. --- .../sytemutvikling/team6/models/Charity.java | 86 +++++++++++-------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Charity.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Charity.java index 2c2cb3f..103fe81 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Charity.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Charity.java @@ -8,11 +8,10 @@ import java.util.ArrayList; import java.util.List; -import java.util.UUID; public class Charity { /* UUID for uniquely identifying each charity */ - private UUID id; + private String org_number; /* Name of the charity */ private String name; @@ -20,11 +19,10 @@ public class Charity { /* Description of the charity's mission and activities */ private String description; - /* Total Donations received */ - private int totalDonations; - /* Is the charity verified? */ - private boolean isVerified; + private String status; + + private boolean is_pre_approved; /* Category for the charity */ private String category; @@ -33,26 +31,61 @@ public class Charity { private List feedbacks; /** - * Konstructor for creating a new charity. The ID is generated automatically using UUID. Total - * donations are initialized to 0. The charity is unverified by default. + * Contructor for creating a new charity. The charity is unverified by default. * - * @param name - * @param description - * @param category + * @param org_number matches from innsamlingkontrollen + * @param name matches from innsamlingkontrollen + * @param is_pre_approved name matches from innsamlingkontrollen + * @param status name matches from innsamlingkontrollen + * @param description no coreleation to innsamlingskontrollen + * @param category no coreleation to innsamlingskontrollen + * + */ - public Charity(String name, String description, String category) { - this.id = UUID.randomUUID(); + public Charity(String org_number, String name, String description, String category, boolean is_pre_approved, String status) { + this.org_number = org_number.replaceAll("\\s", ""); this.name = name; this.description = description; - this.totalDonations = 0; - this.isVerified = false; + this.is_pre_approved = is_pre_approved; + this.status = status; this.feedbacks = new ArrayList<>(); this.category = category; } + /** + * Contructor for creating a new charity. Taylored to match data given from Api. + * Other attributes will just be initialized as empty + * + * @param org_number matches from innsamlingkontrollen + * @param name matches from innsamlingkontrollen + * @param is_pre_approved name matches from innsamlingkontrollen + * @param status name matches from innsamlingkontrollen + * + + */ + public Charity(String org_number, String name, String category, boolean is_pre_approved, String status) { + this.org_number = org_number.replaceAll("\\s", ""); + this.name = name; + this.description = ""; + this.is_pre_approved = is_pre_approved; + this.status = status; + this.feedbacks = new ArrayList<>(); + this.category = ""; + } + /** Getters for the charity's attributes. */ - public UUID getId() { - return id; + public String getOrg_number() { + return org_number; + } + + public String getStatus() { + return status; + } + + public boolean getPreApproved(){return is_pre_approved;} + + public List getFeedbacks() { + return feedbacks; } public String getCategory() { @@ -67,29 +100,14 @@ public String getDescription() { return description; } - public int getTotalDonations() { - return totalDonations; - } - - public boolean isVerified() { - return isVerified; - } - /** Setter for verification status. This one sets the charity as verified. */ public void setVerified() { - this.isVerified = true; + this.status = "Approved"; } /** Setter for verification status. This one sets the charity as unverified. */ public void setUnverified() { - this.isVerified = false; + this.status = "Veto"; } - /** - * Setter for total donations. This method is used to update the total donations when a new - * donation is made. - */ - public void setTotalDonations(int amount) { - this.totalDonations += amount; - } } From 74301088bbf52eccc9f023ce0ea031dd86fb02bf Mon Sep 17 00:00:00 2001 From: AdrianBalunan Date: Wed, 11 Mar 2026 16:02:10 +0100 Subject: [PATCH 048/123] Fix: Unused Variable in Constructor --- .../src/main/java/ntnu/sytemutvikling/team6/models/Charity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Charity.java b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Charity.java index 103fe81..ccd2ce6 100644 --- a/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Charity.java +++ b/helpmehelpapplication/src/main/java/ntnu/sytemutvikling/team6/models/Charity.java @@ -63,7 +63,7 @@ public Charity(String org_number, String name, String description, String catego * */ - public Charity(String org_number, String name, String category, boolean is_pre_approved, String status) { + public Charity(String org_number, String name, boolean is_pre_approved, String status) { this.org_number = org_number.replaceAll("\\s", ""); this.name = name; this.description = ""; From 281684957ffe189ecd77cfed623135712732bd89 Mon Sep 17 00:00:00 2001 From: cathrkri Date: Wed, 11 Mar 2026 16:08:42 +0100 Subject: [PATCH 049/123] feat: scene builder update --- .../src/main/resources/fxml/charityPage.fxml | 780 ++++++++++++------ 1 file changed, 517 insertions(+), 263 deletions(-) diff --git a/helpmehelpapplication/src/main/resources/fxml/charityPage.fxml b/helpmehelpapplication/src/main/resources/fxml/charityPage.fxml index e9c2001..3836445 100644 --- a/helpmehelpapplication/src/main/resources/fxml/charityPage.fxml +++ b/helpmehelpapplication/src/main/resources/fxml/charityPage.fxml @@ -3,7 +3,10 @@ + + + @@ -13,6 +16,7 @@ + @@ -24,279 +28,529 @@ xmlns:fx="http://javafx.com/fxml/1" fx:controller="ntnu.systemutvikling.team6.controller.CharityPageController"> - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + +