diff --git a/pom.xml b/pom.xml
index 75290fc..5bbba22 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,7 +55,7 @@
org.springframeworkspring-core
- 6.1.10
+ 6.2.0org.slf4j
diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java
index be397d8..b9d67da 100644
--- a/src/main/java/edu/group5/app/App.java
+++ b/src/main/java/edu/group5/app/App.java
@@ -5,6 +5,7 @@
import edu.group5.app.model.donation.DonationRepository;
import edu.group5.app.model.donation.DonationService;
import edu.group5.app.model.organization.OrganizationRepository;
+import edu.group5.app.model.organization.OrganizationScraper;
import edu.group5.app.model.organization.OrganizationService;
import edu.group5.app.model.user.UserRepository;
import edu.group5.app.model.user.UserService;
@@ -62,16 +63,16 @@ public void init() {
System.err.println("Failed to load organization data: " + e.getMessage());
}
- // Create repositories with fetched data
+ // Create scraper and repositories with fetched data
+ OrganizationScraper orgScraper = new OrganizationScraper();
this.userRepository = new UserRepository(userData);
this.donationRepository = new DonationRepository(donationData);
- OrganizationRepository organizationRepository = new OrganizationRepository(organizationData);
+ OrganizationRepository organizationRepository = new OrganizationRepository(organizationData, orgScraper);
// Create services (backend wiring)
UserService userService = new UserService(this.userRepository);
DonationService donationService = new DonationService(this.donationRepository, organizationRepository);
- OrganizationService organizationService = new OrganizationService(organizationRepository);
-
+ OrganizationService organizationService = new OrganizationService(organizationRepository, orgScraper);
this.root = new BorderPane();
this.appState = new AppState();
this.nav = new NavigationController(root, appState, userService, donationService, organizationService);
diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java
index 9118c7e..a579a9b 100644
--- a/src/main/java/edu/group5/app/control/DonationController.java
+++ b/src/main/java/edu/group5/app/control/DonationController.java
@@ -12,6 +12,9 @@
import java.util.Map;
import java.util.Set;
+import javafx.scene.control.Alert;
+import javafx.scene.control.ButtonType;
+
public class DonationController {
private final AppState appState;
private final NavigationController nav;
@@ -38,27 +41,58 @@ public Set getUniqueOrgs() {
return uniqueOrgs;
}
- public void handleDonate() {
- // Get session data from MainController
+ public void requestDonationConfirmation() {
+ // Get session data
User currentUser = appState.getCurrentUser();
Organization currentOrg = appState.getCurrentOrganization();
BigDecimal amount = appState.getCurrentDonationAmount();
String paymentMethod = appState.getCurrentPaymentMethod();
+ // Validate before showing dialog
if (currentUser == null) {
- System.err.println("Error: No user logged in");
+ showError("Error: No user logged in");
return;
}
- if (!(currentUser instanceof Customer customer)) {
- System.err.println("Error: Only customers can donate");
+ if (!(currentUser instanceof Customer)) {
+ showError("Error: Only customers can donate");
return;
}
if (currentOrg == null) {
- System.err.println("Error: No organization selected");
+ showError("Error: No organization selected");
return;
}
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
- System.err.println("Error: Invalid donation amount");
+ showError("Please select a donation amount first");
+ return;
+ }
+
+ // Show confirmation dialog
+ Alert confirmDialog = new Alert(Alert.AlertType.CONFIRMATION);
+ confirmDialog.setTitle("Confirm Donation");
+ confirmDialog.setHeaderText("Confirm Your Donation");
+ confirmDialog.setContentText(
+ "Organization: " + currentOrg.name() + "\n" +
+ "Amount: " + amount + " kr\n" +
+ "Payment Method: " + paymentMethod + "\n\n" +
+ "Are you sure you want to proceed?"
+ );
+
+ // If user clicks OK, process donation
+ if (confirmDialog.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
+ handleDonate();
+ }
+ // If Cancel, dialog just closes and nothing happens
+ }
+
+ private void handleDonate() {
+ // This now only handles the actual donation processing
+ User currentUser = appState.getCurrentUser();
+ Organization currentOrg = appState.getCurrentOrganization();
+ BigDecimal amount = appState.getCurrentDonationAmount();
+ String paymentMethod = appState.getCurrentPaymentMethod();
+
+ if (!(currentUser instanceof Customer customer)) {
+ System.err.println("Error: Only customers can donate");
return;
}
if (paymentMethod == null) {
@@ -86,4 +120,12 @@ public void handleDonate() {
// Navigate to payment complete
nav.showPaymentCompletePage();
}
+
+ private void showError(String message) {
+ Alert errorAlert = new Alert(Alert.AlertType.WARNING);
+ errorAlert.setTitle("Donation Error");
+ errorAlert.setHeaderText("Cannot Process Donation");
+ errorAlert.setContentText(message);
+ errorAlert.showAndWait();
+ }
}
diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java
index cc0a6b1..61155b7 100644
--- a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java
+++ b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java
@@ -11,25 +11,33 @@
* Repository class for managing Organization entities. It provides methods to retrieve trusted organizations,
* find organizations by their organization number or name, and initializes the repository with input data.
* The repository uses a HashMap to store Organization objects for efficient retrieval based on their organization number.
- * Handles the business logic associated with organizations
+ * Delegates web scraping to OrganizationScraper for separation of concerns.
*/
public class OrganizationRepository extends Repository {
private final HashMap grandMap;
+ private final OrganizationScraper scraper;
/**
- * Initializes the repository with the given input data, c
- * onverting it into Organization objects and storing them in a map for efficient retrieval.
- * The input is expected to be an array of objects, where each object contains
+ * Initializes the repository with the given input data and scraper.
+ * Converts input into Organization objects and stores them in a map for efficient retrieval.
+ * The input is expected to be an array of objects, where each object contains
* the necessary information to create an Organization.
+ *
* @param input the input data used to populate the repository, must not be null
- * @throws IllegalArgumentException if the input is null
+ * @param scraper the OrganizationScraper to use for fetching web data, must not be null
+ * @throws IllegalArgumentException if input or scraper is null
*/
- public OrganizationRepository(Object[] input) {
- super(new HashMap<>());
- grandMap = new HashMap<>();
+ public OrganizationRepository(Object[] input, OrganizationScraper scraper) {
+ super(new HashMap<>());
+ this.grandMap = new HashMap<>();
if (input == null) {
throw new IllegalArgumentException("The input cannot be null");
}
+ if (scraper == null) {
+ throw new IllegalArgumentException("The scraper cannot be null");
+ }
+ this.scraper = scraper;
+
ObjectMapper mapper = new ObjectMapper();
for (Object obj : input) {
@@ -42,7 +50,8 @@ public OrganizationRepository(Object[] input) {
boolean trusted = "approved".equalsIgnoreCase((String) contentMap.get("status"));
String websiteURL = (String) contentMap.get("url");
boolean isPreApproved = Boolean.TRUE.equals(contentMap.get("is_pre_approved"));
- String description = "Information about " + name;
+ String description = scraper.fetchDescription(websiteURL);
+ description = description != null ? description : "Information about " + name;
Organization org = new Organization(orgNumber, name, trusted, websiteURL, isPreApproved, description, null);
grandMap.put(org.orgNumber(), org);
diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java
new file mode 100644
index 0000000..6a8c230
--- /dev/null
+++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java
@@ -0,0 +1,115 @@
+package edu.group5.app.model.organization;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.TextNode;
+import org.jsoup.select.Elements;
+
+import java.util.stream.Collectors;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Handles web scraping of organization information from Innsamlingskontrollen.
+ * Responsible for fetching logos and descriptions from organization pages.
+ * All results are cached to avoid redundant network requests.
+ */
+public class OrganizationScraper {
+ private final Map logoCache = new HashMap<>();
+ private final Map descriptionCache = new HashMap<>();
+
+ /**
+ * Fetches the description for the given URL by scraping all text content
+ * inside {@code }. Results are cached.
+ *
+ *
Strategy:
+ *
+ *
Tries to get all <p> tags (skipping the first one) and concatenates them
+ *
If no paragraphs found, gets all text content from the section
+ *
Returns null if section not found or is empty
+ *
+ *
+ * @param pageUrl the URL for the organization's page; may be null or blank
+ * @return the description text, or null if not found or pageUrl is invalid
+ */
+ public String fetchDescription(String pageUrl) {
+ if (pageUrl == null || pageUrl.isBlank()) {
+ return null;
+ }
+
+ if (descriptionCache.containsKey(pageUrl)) {
+ return descriptionCache.get(pageUrl);
+ }
+
+ try {
+ Document doc = Jsoup.connect(pageUrl)
+ .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
+ .timeout(5000).get();
+
+ Element section = doc.selectFirst("section.information");
+ if (section != null) {
+ section.select("div.extra-info").remove();
+ section.select("a.read-more").remove();
+
+ // Extract all
tags and
elements as separate paragraphs
+ String description = section.select("p, div").stream()
+ .filter(el -> el.tagName().equals("p") || el.select("p").isEmpty())
+ .filter(el -> !el.hasClass("extra-info") && !el.hasClass("logo"))
+ .map(Element::text)
+ .map(text -> text.replace("Les mer", "").trim())
+ .filter(text -> !text.isBlank())
+ .collect(Collectors.joining("\n\n"));
+
+ // Fallback: if no paragraphs found, get all text from section
+ if (description.isBlank()) {
+ description = section.text().trim();
+ }
+ description = description.replace("Les mer", "").trim();
+
+ // Only cache and return if we found something meaningful
+ if (!description.isBlank()) {
+ descriptionCache.put(pageUrl, description);
+ return description;
+ }
+ }
+ } catch (Exception e) {
+ System.out.println("Could not get description for: " + pageUrl);
+ }
+ return null;
+ }
+
+ /**
+ * Fetches the logo URL for the given page by scraping the {@code div.logo img}
+ * element. Results are cached so each URL is only fetched once.
+ *
+ * @param pageUrl the URL for the organization's page; may be null or blank
+ * @return the absolute logo URL, or null if not found or pageUrl is invalid
+ */
+ public String fetchLogoUrl(String pageUrl) {
+ if (pageUrl == null || pageUrl.isBlank()) {
+ return null;
+ }
+
+ if (logoCache.containsKey(pageUrl)) {
+ return logoCache.get(pageUrl);
+ }
+
+ try {
+ Document doc = Jsoup.connect(pageUrl)
+ .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
+ .timeout(5000).get();
+ Element img = doc.selectFirst("div.logo img");
+
+ if (img != null) {
+ String logoUrl = img.absUrl("src");
+ logoCache.put(pageUrl, logoUrl);
+ return logoUrl;
+ }
+ } catch (Exception e) {
+ System.out.println("Could not get logo for: " + pageUrl);
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java
index 9785040..e19c273 100644
--- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java
+++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java
@@ -1,10 +1,9 @@
package edu.group5.app.model.organization;
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
+import java.util.stream.Collectors;
-import java.util.HashMap;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@@ -13,26 +12,29 @@
* It interacts with the OrganizationRepository to retrieve organization information
* and contains business logic associated with organization management.
*
- *
It provides fetching logo URLs by web scraping each organization's page on
- * Innsamlingskontrollen.
+ *
It provides fetching logo URLs by delegating to OrganizationScraper.
*
* Fetched logo URLs are cached to avoid redundant network requests.
*/
public class OrganizationService {
private OrganizationRepository organizationRepository;
-
- private final Map logoCache = new HashMap<>();
+ private OrganizationScraper scraper;
/**
- * Constructs an OrganizationService with the given OrganizationRepository.
+ * Constructs an OrganizationService with the given OrganizationRepository and scraper.
* @param organizationRepository the OrganizationRepository to use for managing organization data; must not be null
- * @throws IllegalArgumentException if organizationRepository is null
+ * @param scraper the OrganizationScraper to use for fetching web data; must not be null
+ * @throws IllegalArgumentException if organizationRepository or scraper is null
*/
- public OrganizationService(OrganizationRepository organizationRepository) {
+ public OrganizationService(OrganizationRepository organizationRepository, OrganizationScraper scraper) {
if (organizationRepository == null) {
throw new IllegalArgumentException("OrganizationRepository cannot be null");
}
+ if (scraper == null) {
+ throw new IllegalArgumentException("OrganizationScraper cannot be null");
+ }
this.organizationRepository = organizationRepository;
+ this.scraper = scraper;
}
/**
@@ -48,7 +50,10 @@ public OrganizationRepository getOrganizationRepository() {
* @return a map of trusted organizations by organization number
*/
public Map getTrustedOrganizations() {
- return organizationRepository.getTrustedOrganizations();
+ return organizationRepository.getTrustedOrganizations().values().stream()
+ .sorted(Comparator.comparing(Organization::name))
+ .collect(Collectors.toMap(Organization::orgNumber,
+ org -> org, (e1, e2) -> e1, LinkedHashMap::new));
}
/**
@@ -80,59 +85,30 @@ public Organization findByOrgName(String name) {
* @return the absolute logo URL, or null if not found or pageUrl is invalid
*/
- public String fetchLogoUrl(String pageUrl) {
- if (pageUrl == null || pageUrl.isBlank()) {
- return null;
- }
-
- if (logoCache.containsKey(pageUrl)) {
- return logoCache.get(pageUrl);
- }
-
- try {
- Document doc = Jsoup.connect(pageUrl).get();
- Element img = doc.selectFirst("div.logo img");
-
- if (img != null) {
- String logoUrl = img.absUrl("src");
- logoCache.put(pageUrl, logoUrl);
- return logoUrl;
- }
- } catch (Exception e) {
- System.out.println("Could not get logo for: " + pageUrl);
- }
- return null;
- }
-
/**
- * Fetches all trusted organizations with their logo URLs.
+ * Fetches all trusted organizations with their logo URLs and descriptions.
*
*
- * For each trusted organization, attempts to get its logo using
- * {@link #fetchLogoUrl(String)}. Creates a new Organization
- * object including the logo URL.
+ * For each trusted organization, attempts to get its logo using the scraper.
+ * Creates a new Organization object including the logo URL (description is
+ * already fetched during repository initialization).
*