Skip to content

Commit

Permalink
fix[merge]: resolve conflicts and fix method name mismatches
Browse files Browse the repository at this point in the history
  • Loading branch information
Fredrik Marjoni committed Apr 16, 2026
2 parents 69ec247 + 3904539 commit 2d3df56
Show file tree
Hide file tree
Showing 40 changed files with 1,432 additions and 474 deletions.
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
38 changes: 29 additions & 9 deletions src/main/java/edu/group5/app/control/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
import edu.group5.app.model.user.User;
import edu.group5.app.model.user.UserService;
import edu.group5.app.view.loginpage.LoginPageView;
import edu.group5.app.view.loginpage.SignInPageView;
import edu.group5.app.view.loginpage.SignUpPageView;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;

import java.util.Arrays;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
Expand Down Expand Up @@ -49,7 +55,7 @@ public AuthController(AppState appState, NavigationController nav, UserService u
* @param email the user's email
* @param passwordChars the user's password
*/
public void handleSignUp(SignInPageView view, String firstName, String lastName, String email, char[] passwordChars) {
public void handleSignUp(SignUpPageView view, String firstName, String lastName, String email, char[] passwordChars) {
if (firstName == null || firstName.trim().isEmpty() ||
lastName == null || lastName.trim().isEmpty() ||
email == null || email.trim().isEmpty() ||
Expand All @@ -58,22 +64,36 @@ public void handleSignUp(SignInPageView view, String firstName, String lastName,
return;
}

String password = new String(passwordChars);
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String hashedPassword = encoder.encode(password);

boolean success = userService.registerUser(
"Customer", firstName, lastName, email, hashedPassword);
// Clears password char array after creating a hash.
String hashedPassword = encoder.encode(new String(passwordChars));
for (int i = 0; i < passwordChars.length; i++) {
passwordChars[0] = '0';
}

Alert privacyPolicy = new Alert(Alert.AlertType.CONFIRMATION);
privacyPolicy.setTitle("Accept Privacy Policy");
privacyPolicy.setHeaderText("Accept Privacy Policy");
privacyPolicy.setContentText(
"Your user information like:\n" +
"Name and email—as well as donations tied to your account—will be saved locally on your machine.\n" +
"By creating an account, you accept the right of our app to store this information.");

if (success) {
User user = userService.getUserByEmail(email);
if (privacyPolicy.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
boolean success = userService.registerUser(
"Customer", firstName, lastName, email, hashedPassword);

if (success) {
User user = userService.getUserByEmail(email);

appState.setCurrentUser(user);
nav.showHomePage();
} else {
view.showError("Registration failed. Email may already be in use.");
}
}
}

/**
* Handles the login of a {@link User}.
Expand Down Expand Up @@ -120,4 +140,4 @@ public void handleLogout() {
appState.setCurrentPaymentMethod(null);
nav.showLoginPage();
}
}
}
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;

/**
* Controller responsible for donation-related operations.
*
Expand Down Expand Up @@ -75,27 +78,58 @@ public Set<Integer> getUniqueOrganizationIDs() {
* </ul>
* </p>
*/
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 @@ -123,4 +157,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,7 +11,7 @@
import edu.group5.app.view.homepage.HomePageView;
import edu.group5.app.view.loginpage.LoginHeader;
import edu.group5.app.view.loginpage.LoginPageView;
import edu.group5.app.view.loginpage.SignInPageView;
import edu.group5.app.view.loginpage.SignUpPageView;
import edu.group5.app.view.organizationpage.OrganizationPageView;
import edu.group5.app.view.userpage.UserPageView;
import javafx.scene.layout.BorderPane;
Expand Down Expand Up @@ -52,9 +52,9 @@ public void showLoginPage() {
root.setCenter(new LoginPageView(appState, this, authController));
}

public void showSignInPage() {
public void showSignUpPage() {
root.setTop(loginHeader);
root.setCenter(new SignInPageView(appState, this, authController));
root.setCenter(new SignUpPageView(appState, this, authController));
}

public void showPaymentCompletePage() {
Expand Down
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

0 comments on commit 2d3df56

Please sign in to comment.