Skip to content

Commit

Permalink
feat&update[OrganizationPage]: Update description to be description f…
Browse files Browse the repository at this point in the history
…etched from the API
  • Loading branch information
Fredrik Marjoni committed Apr 8, 2026
1 parent c5c3ce9 commit a2a087b
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 73 deletions.
8 changes: 5 additions & 3 deletions src/main/java/edu/group5/app/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
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,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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<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) {
// Try to get all <p> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,26 +11,29 @@
* It interacts with the OrganizationRepository to retrieve organization information
* and contains business logic associated with organization management.
*
* <P>It provides fetching logo URLs by web scraping each organization's page on
* Innsamlingskontrollen.</P>
* <P>It provides fetching logo URLs by delegating to OrganizationScraper.</P>
*
* Fetched logo URLs are cached to avoid redundant network requests.
*/
public class OrganizationService {
private OrganizationRepository organizationRepository;

private final Map<String, String> 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;
}

/**
Expand Down Expand Up @@ -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.
*
* <p>
* 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).
* </p>
* @return a map of trusted organizations keyed by organization number, with logos included
*/
Expand All @@ -127,7 +101,7 @@ public Map<Integer, Organization> getTrustedOrganizationsWithLogos() {
org.websiteUrl(),
org.isPreApproved(),
org.description(),
fetchLogoUrl(org.websiteUrl())
scraper.fetchLogoUrl(org.websiteUrl())
))
.collect(Collectors.toMap(Organization::orgNumber, org -> org));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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");
Expand Down
44 changes: 29 additions & 15 deletions src/main/resources/organizationpage/organizationpage.css
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit a2a087b

Please sign in to comment.