Skip to content

Perf/browsepage #58

Merged
merged 8 commits into from
Mar 26, 2026
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@
<version>2.2.224</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.17.2</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
*
* <p>
* An organization is identified by an organization number, a name,
* trust status, website Url, pre-approval status, and a textual description.
* trust status, website Url, pre-approval status, and a textual description,
* and a logo URL.
*
* <p>
* Instances are validated on creation:
* <ul>
* <li>orgNumber must be non-negative</li>
* <li>name and websiteUrl must not be null or blank</li>
* <li>description must not be null</li>
* <li>logoUrl may be null if no logo is available</li>
* </ul>
*/
public record Organization(
Expand All @@ -23,7 +25,8 @@ public record Organization(
boolean trusted,
String websiteUrl,
boolean isPreApproved,
String description) {
String description,
String logoUrl) {
/**
* Creates a new organization.
*
Expand All @@ -35,12 +38,13 @@ public record Organization(
* @param isPreApproved whether the organization is pre-approved
* @param description a textual description of the organization; must not be
* null
* @param logoUrl the URL to the organization's logo image; may be null
* @throws NullPointerException if name, websiteUrl or description is null
* @throws IllegalArgumentException if orgNumber is negative, or if name or
* websiteUrl is blank
*/
public Organization(int orgNumber, String name, boolean trusted, String websiteUrl, boolean isPreApproved,
String description) {
String description, String logoUrl) {
if (orgNumber < 0) {
throw new IllegalArgumentException("orgNumber cannot be negative");
}
Expand All @@ -50,6 +54,7 @@ public Organization(int orgNumber, String name, boolean trusted, String websiteU
this.websiteUrl = Objects.requireNonNull(websiteUrl, "websiteUrl cannot be null");
this.isPreApproved = isPreApproved;
this.description = Objects.requireNonNull(description, "description cannot be null");
this.logoUrl = logoUrl;

if (name.isBlank()) {
throw new IllegalArgumentException("name cannot be blank");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public OrganizationRepository(Object[] input) {
String websiteURL = (String) contentMap.get("url");
boolean isPreApproved = Boolean.TRUE.equals(contentMap.get("is_pre_approved"));
String description = "Information about " + name;
Organization org = new Organization(orgNumber, name, trusted, websiteURL, isPreApproved, description);
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
@@ -1,15 +1,28 @@
package edu.group5.app.model.organization;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

/**
* Service class for managing organization-related operations.
* 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>
*
* Fetched logo URLs are cached to avoid redundant network requests.
*/
public class OrganizationService {
private OrganizationRepository organizationRepository;

private final Map<String, String> logoCache = new HashMap<>();

/**
* Constructs an OrganizationService with the given OrganizationRepository.
* @param organizationRepository the OrganizationRepository to use for managing organization data; must not be null
Expand Down Expand Up @@ -55,4 +68,82 @@ public Organization findByOrgNumber(int orgNumber) {
public Organization findByOrgName(String name) {
return organizationRepository.findByOrgName(name);
}

/**
* 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.
*
* <P>
* Using Jsoup to web scrape through the URLs in the API.
* </P>
* @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).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.
*
* <p>
* For each trusted organization, attempts to get its logo using
* {@link #fetchLogoUrl(String)}. Creates a new Organization
* object including the logo URL.
* </p>
* @return a map of trusted organizations keyed by organization number, with logos included
*/
public Map<Integer, Organization> getTrustedOrganizationsWithLogos() {
Map<Integer, Organization> original = getTrustedOrganizations();
Map<Integer, Organization> trustedOrgsWithLogos = new HashMap<>();

for (Organization org : original.values()) {
String logoUrl = fetchLogoUrl(org.websiteUrl());

Organization newOrg = new Organization(
org.orgNumber(),
org.name(),
org.trusted(),
org.websiteUrl(),
org.isPreApproved(),
org.description(),
logoUrl
);
trustedOrgsWithLogos.put(newOrg.orgNumber(), newOrg);
}
return trustedOrgsWithLogos;
}

/**
* Asynchronously fetches trusted organizations with logos.
*
* <p>Runs in the background so the UI thread is no blocked.
* Returns a CompletableFuture that completes when all logos are loaded.</p>
*
* @return a CompletableFuture containing a map of organizations with logos
*/
public CompletableFuture<Map<Integer, Organization>> getTrustedOrganizationsWithLogosAsync() {
return CompletableFuture.supplyAsync(this::getTrustedOrganizationsWithLogos);
}
}
28 changes: 20 additions & 8 deletions src/main/java/edu/group5/app/view/browsepage/BrowseCard.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,32 @@ public BrowseCard(BrowseCardController browseCardController, Organization org, S
private StackPane imageContainer(String img) {
StackPane imageContainer = new StackPane();
imageContainer.setId("imageContainer");

imageContainer.setPrefHeight(80);
imageContainer.setPrefWidth(80);
imageContainer.setMaxWidth(Double.MAX_VALUE);

ImageView logo = new ImageView(
new Image(getClass().getResource(img).toExternalForm())
);

logo.setId("logo");
logo.setSmooth(true);
logo.setPreserveRatio(true);
logo.setFitHeight(80);
if (img != null && !img.isBlank()) {
ImageView logo = new ImageView(new Image(img, true));
logo.setId("logo");
logo.setSmooth(true);
logo.setPreserveRatio(true);
logo.setFitHeight(80);
logo.setFitWidth(80);

imageContainer.getChildren().add(logo);
} else {
StackPane placeholder = new StackPane();
placeholder.setPrefSize(80, 80);

Text text = new Text("No image");
text.setStyle("-fx-font-size: 10;");

placeholder.getChildren().add(text);
imageContainer.getChildren().add(placeholder);
}

imageContainer.getChildren().add(logo);
return imageContainer;
}

Expand Down
35 changes: 26 additions & 9 deletions src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import edu.group5.app.control.MainController;
import edu.group5.app.model.organization.Organization;
import edu.group5.app.view.Header;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
Expand Down Expand Up @@ -75,8 +76,26 @@ private GridPane createOrganizationSection(String searchTerm) {
grid.setStyle("-fx-padding: 0;");
grid.setMaxWidth(Double.MAX_VALUE);

// Store reference for later updates
if (organizationGrid == null) {
organizationGrid = grid;
}

if (allOrganizations == null) {
allOrganizations = mainController.getOrganizationService().getTrustedOrganizations();

//Show loading text while organizations and logos are fetched
grid.add(new javafx.scene.control.Label("Loading..."), 0, 0);

//Fetch trusted organizations with logos asynchronously (runs in background)
mainController.getOrganizationService()
.getTrustedOrganizationsWithLogosAsync()
.thenAccept(orgs -> {
this.allOrganizations = orgs;

// Update UI when data is ready
Platform.runLater(() -> updateOrganizationGrid(""));
});
return grid;
}

// Filter organizations by search term
Expand All @@ -86,13 +105,16 @@ private GridPane createOrganizationSection(String searchTerm) {
int row = 0;

for (Organization org : organizations.values()) {
String defaultImg = "/browsepage/images/children_of_shambala.png";
BrowseCard card = new BrowseCard(orgController, org, defaultImg);
//Adds default text if organization does not have any
String img = (org.logoUrl() != null && !org.logoUrl().isBlank())
? org.logoUrl()
: null;

BrowseCard card = new BrowseCard(orgController, org, img);

grid.add(card, column, row);

column++;

if (column == 4) {
column = 0;
row++;
Expand All @@ -105,11 +127,6 @@ private GridPane createOrganizationSection(String searchTerm) {
grid.getColumnConstraints().add(col);
}

// Store reference for later updates
if (organizationGrid == null) {
organizationGrid = grid;
}

return grid;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,21 @@ private StackPane createImageContainer() {
imageContainer.setMaxWidth(Double.MAX_VALUE);

Organization org = mainController.getCurrentOrganization();
String imagePath = org != null ? "/browsepage/images/children_of_shambala.png" : "/browsepage/images/children_of_shambala.png";

ImageView logo = new ImageView(
new Image(getClass().getResource(imagePath).toExternalForm())
);

logo.setId("logo");
logo.setSmooth(true);
logo.setPreserveRatio(true);

imageContainer.getChildren().add(logo);
if (org != null && org.logoUrl() != null && !org.logoUrl().isBlank()) {
ImageView logo = new ImageView(new Image(org.logoUrl(), true));
logo.setId("logo");
logo.setSmooth(true);
logo.setPreserveRatio(true);
imageContainer.getChildren().add(logo);
} else {
StackPane placeholder = new StackPane();

Text text = new Text("No image");
text.setStyle("-fx-font-size: 10;");

placeholder.getChildren().add(text);
imageContainer.getChildren().add(placeholder);
}
return imageContainer;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ void getTrustedOrganizations_OnlyReturnsTrustedOrganizations() {
@Test
void testFindByOrgNumberReturnsOrganization() {
assertEquals(new Organization(1, "Trusted Org1", true,
"org.com", true, "Information about Trusted Org1"),
"org.com", true, "Information about Trusted Org1", null),
repository.findByOrgNumber(1));
}

Expand All @@ -93,7 +93,7 @@ void testFindByOrgNumberIfOrgNumberNotFound() {
@Test
void testFindByOrgNameReturnsOrganization() {
assertEquals(new Organization(1, "Trusted Org1", true,
"org.com", true, "Information about Trusted Org1"),
"org.com", true, "Information about Trusted Org1", null),
repository.findByOrgName("Trusted Org1"));
}

Expand All @@ -116,7 +116,7 @@ void testFindByOrgNameIfNameNotFound() {
@Test
void testFindByOrgNameIsCaseInsensitive() {
assertEquals(new Organization(1, "Trusted Org1", true,
"org.com", true, "Information about Trusted Org1"),
"org.com", true, "Information about Trusted Org1", null),
repository.findByOrgName("trusted org1"));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,19 @@ void testFindByOrgName() {
assertEquals(1, org.orgNumber());
assertEquals("Misjonsalliansen", org.name());
}

@Test
void fetchLogoUrlReturnsNullWhenUrlIsNull() {
assertNull(service.fetchLogoUrl(null));
}
@Test
void fetchLogoUrlReturnsNullWhenUrlIsBlank() {
assertNull(service.fetchLogoUrl(""));
}
@Test
void fetchLogoUrlCachesResultOnSecondCall() {
String result1 = service.fetchLogoUrl("https://");
String result2 = service.fetchLogoUrl("https://");
assertEquals(result1, result2);
}
}
Loading