Skip to content

Merge Update/organization/description into release/v2.0.0 #66

Merged
merged 18 commits into from
Apr 14, 2026
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2f11b68
update: update speed of rendering OrgCards and Org image with Paralle…
Apr 5, 2026
ed93b95
update&perf[App]: Update and infcreased performance of org.logos rend…
Apr 5, 2026
c5c3ce9
fix[App]: remove preloading redundancy
Apr 8, 2026
a2a087b
feat&update[OrganizationPage]: Update description to be description f…
Apr 8, 2026
456a1d6
update&test[Organization]: Update JUnit tests with new features regar…
Apr 8, 2026
fbc4ed1
update[Organization]: Update Hashmap of Organizations to be displayed…
Apr 9, 2026
fe43461
update[CausesPage]: Update CausesPage to have search bare fixed at th…
Apr 9, 2026
9c6134c
Merge branch 'feat/perf/ParameterValidator' into update/organization/…
Apr 9, 2026
85d6951
update[UserPage]: Update Userpage with more visual appealing Javafx d…
Apr 9, 2026
9d915ba
Update[UserPage]: Update Visual on UserPage to increase greater UX an…
Apr 9, 2026
cfef39a
fix[]UserPage: fix sizing of donation section
Apr 9, 2026
427f4e5
Step 3: Upgrade Spring Framework Dependency - Compile: SUCCESS
Apr 10, 2026
441155e
update[OrganizationPage]: Update OrganizationPage's description to fe…
Apr 10, 2026
4edd823
fix&update[OrganizationPage]: Fix duplication and update paragraph sp…
Apr 10, 2026
5767aaa
update[DonationPage]: Refactor donation selection, add confirmation d…
Apr 12, 2026
b1a1fb8
feat&Update[donationPage]: Add PaymentMethod and backButton from rele…
Apr 14, 2026
40b3ba9
Merge branch 'release/v2.0.0' into update/organization/description
fredrjm Apr 14, 2026
0556c5b
fix&Update[DonationPage]: fix up merge conflict problems and update d…
Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.1.10</version>
<version>6.2.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
Expand Down
9 changes: 5 additions & 4 deletions src/main/java/edu/group5/app/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
56 changes: 49 additions & 7 deletions src/main/java/edu/group5/app/control/DonationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,27 +41,58 @@ public Set<Integer> 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) {
Expand Down Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Integer, Organization> {
private final HashMap<Integer, Organization> 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) {
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> logoCache = new HashMap<>();
private final Map<String, String> descriptionCache = new HashMap<>();

/**
* Fetches the description for the given URL by scraping all text content
* inside {@code <section class="information">}. Results are cached.
*
* <p>Strategy:</p>
* <ol>
* <li>Tries to get all &lt;p&gt; tags (skipping the first one) and concatenates them</li>
* <li>If no paragraphs found, gets all text content from the section</li>
* <li>Returns null if section not found or is empty</li>
* </ol>
*
* @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 <p> tags and <div> 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;
}
}
Loading