From a2a087b70b55435d5293f890b17bf1c00b1b84f0 Mon Sep 17 00:00:00 2001 From: Fredrik Marjoni Date: Wed, 8 Apr 2026 15:17:55 +0200 Subject: [PATCH] feat&update[OrganizationPage]: Update description to be description fetched from the API --- src/main/java/edu/group5/app/App.java | 8 +- .../organization/OrganizationRepository.java | 26 +++-- .../organization/OrganizationScraper.java | 107 ++++++++++++++++++ .../organization/OrganizationService.java | 56 +++------ .../OrganizationPageView.java | 24 +++- .../organizationpage/organizationpage.css | 44 ++++--- 6 files changed, 192 insertions(+), 73 deletions(-) create mode 100644 src/main/java/edu/group5/app/model/organization/OrganizationScraper.java diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index fa48178..b59c6ef 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -7,6 +7,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,15 +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/model/organization/OrganizationRepository.java b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java index cc0a6b1..f31efdc 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,7 @@ 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) != null ? scraper.fetchDescription(websiteURL) : "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..edb41fd --- /dev/null +++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java @@ -0,0 +1,107 @@ +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.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:

+ *
    + *
  1. Tries to get all <p> tags (skipping the first one) and concatenates them
  2. + *
  3. If no paragraphs found, gets all text content from the section
  4. + *
  5. Returns null if section not found or is empty
  6. + *
+ * + * @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) { + // Try to get all

tags (skip first one if multiple exist) + String description = section.select("p").stream() + .skip(1) // Skip first paragraph (usually a heading) + .map(Element::text) + .filter(text -> !text.isBlank()) + .map(String::trim) + .collect(Collectors.joining("\n\n")); + + // Fallback: if no paragraphs after first, get all text from section + if (description.isBlank()) { + description = section.text().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 8ebd625..170b9a5 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -1,8 +1,5 @@ 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; @@ -14,26 +11,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; } /** @@ -81,39 +81,13 @@ 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) - .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; - } - /** - * 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). *

* @return a map of trusted organizations keyed by organization number, with logos included */ @@ -127,7 +101,7 @@ public Map getTrustedOrganizationsWithLogos() { org.websiteUrl(), org.isPreApproved(), org.description(), - fetchLogoUrl(org.websiteUrl()) + scraper.fetchLogoUrl(org.websiteUrl()) )) .collect(Collectors.toMap(Organization::orgNumber, org -> org)); } diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 1ac2fa6..2d10328 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -8,6 +8,7 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextArea; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; @@ -68,8 +69,8 @@ private StackPane createImageContainer() { logo.setId("logo"); logo.setSmooth(true); logo.setPreserveRatio(true); - logo.setFitHeight(120); - logo.setFitWidth(120); + logo.setFitHeight(350); + logo.setFitWidth(350); imageContainer.getChildren().add(logo); } else { StackPane placeholder = new StackPane(); @@ -90,14 +91,27 @@ private VBox createOrgInfoSection() { orgInfoSection.setSpacing(50); VBox orgNameAndDescription = new VBox(); + orgNameAndDescription.setSpacing(5); Label orgName = new Label(org != null ? org.name() : "Unknown Organization"); orgName.setId("orgName"); - Text description = new Text(org != null ? org.description() : "No description available"); - description.setId("description"); + VBox descriptionBox = new VBox(); + descriptionBox.setSpacing(15); + descriptionBox.setId("description-container"); + + if (org != null && org.description() != null) { + String[] paragraphs = org.description().split("\n\n"); + for (String para : paragraphs) { + if (!para.isBlank()) { + Label paragraph = new Label(para.trim()); + paragraph.setWrapText(true); + descriptionBox.getChildren().add(paragraph); + } + } + } - orgNameAndDescription.getChildren().addAll(orgName, description); + orgNameAndDescription.getChildren().addAll(orgName, descriptionBox); Button donateBtn = new Button("Donate"); donateBtn.setId("donate-button"); diff --git a/src/main/resources/organizationpage/organizationpage.css b/src/main/resources/organizationpage/organizationpage.css index a7276b5..e5263f2 100644 --- a/src/main/resources/organizationpage/organizationpage.css +++ b/src/main/resources/organizationpage/organizationpage.css @@ -1,31 +1,45 @@ #main-container { - -fx-padding: 50px + -fx-padding: 50px } #logo { - -fx-min-height: 50%; + -fx-min-height: 80%; } #orgName { - -fx-font-weight: bold; - -fx-font-size: 20pt; + -fx-font-weight: bold; + -fx-font-size: 28pt; + -fx-padding: 0 0 30 0; } -#description { - -fx-font-size: 10pt; +#description-container { + -fx-padding: 30 0 30 0; + -fx-spacing: 25; +} + +#description-paragraph { + -fx-font-size: 16; + -fx-text-fill: #222; + -fx-font-family: "Segoe UI", Arial, sans-serif; + -fx-padding: 15 50 15 50; + -fx-line-spacing: 6; + -fx-text-alignment: Left; + -fx-wrap-text: true; + } #donate-button { - -fx-pref-height: 55px; - -fx-background-color: #e03030; - -fx-text-fill: white; - -fx-font-size: 22px; - -fx-font-weight: bold; - -fx-background-radius: 8; - -fx-cursor: hand; - -fx-padding: 0 40 0 40; + -fx-pref-height: 55px; + -fx-background-color: #e03030; + -fx-text-fill: white; + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-padding: 0 40 0 40; + -fx-margin-top: 30; } #donate-button:hover { - -fx-background-color: #c02020; + -fx-background-color: #c02020; } \ No newline at end of file