Skip to content

Commit

Permalink
Merge pull request #58 from Group-5/perf/browsepage
Browse files Browse the repository at this point in the history
Perf/browsepage
  • Loading branch information
fredrjm authored Mar 26, 2026
2 parents 16b02a1 + 5db4ec6 commit 80f5044
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 41 deletions.
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

0 comments on commit 80f5044

Please sign in to comment.