diff --git a/pom.xml b/pom.xml index f9a8b88..75290fc 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,11 @@ 2.2.224 runtime + + org.jsoup + jsoup + 1.17.2 + diff --git a/src/main/java/edu/group5/app/model/organization/Organization.java b/src/main/java/edu/group5/app/model/organization/Organization.java index 42844e1..7016567 100644 --- a/src/main/java/edu/group5/app/model/organization/Organization.java +++ b/src/main/java/edu/group5/app/model/organization/Organization.java @@ -7,7 +7,8 @@ * *

* 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. * *

* Instances are validated on creation: @@ -15,6 +16,7 @@ *

  • orgNumber must be non-negative
  • *
  • name and websiteUrl must not be null or blank
  • *
  • description must not be null
  • + *
  • logoUrl may be null if no logo is available
  • * */ public record Organization( @@ -23,7 +25,8 @@ public record Organization( boolean trusted, String websiteUrl, boolean isPreApproved, - String description) { + String description, + String logoUrl) { /** * Creates a new organization. * @@ -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"); } @@ -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"); 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 a47b3d5..cc0a6b1 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java @@ -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); } 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 c5979f5..9785040 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -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. + * + *

    It provides fetching logo URLs by web scraping each organization's page on + * Innsamlingskontrollen.

    + * + * Fetched logo URLs are cached to avoid redundant network requests. */ public class OrganizationService { private OrganizationRepository organizationRepository; + private final Map logoCache = new HashMap<>(); + /** * Constructs an OrganizationService with the given OrganizationRepository. * @param organizationRepository the OrganizationRepository to use for managing organization data; must not be null @@ -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. + * + *

    + * Using Jsoup to web scrape through the URLs in the API. + *

    + * @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. + * + *

    + * For each trusted organization, attempts to get its logo using + * {@link #fetchLogoUrl(String)}. Creates a new Organization + * object including the logo URL. + *

    + * @return a map of trusted organizations keyed by organization number, with logos included + */ + public Map getTrustedOrganizationsWithLogos() { + Map original = getTrustedOrganizations(); + Map 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. + * + *

    Runs in the background so the UI thread is no blocked. + * Returns a CompletableFuture that completes when all logos are loaded.

    + * + * @return a CompletableFuture containing a map of organizations with logos + */ + public CompletableFuture> getTrustedOrganizationsWithLogosAsync() { + return CompletableFuture.supplyAsync(this::getTrustedOrganizationsWithLogos); + } } diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java index ad4be8a..b529e89 100644 --- a/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java +++ b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java @@ -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; } diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java b/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java index a2d92c8..b27b011 100644 --- a/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java +++ b/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java @@ -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; @@ -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 @@ -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++; @@ -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; } 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 4ec7c91..b0afa27 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -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; } diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java index 7a5ece5..f821e35 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java @@ -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)); } @@ -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")); } @@ -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")); } diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java index e34aba7..0920e67 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java @@ -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); + } } diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationTest.java index f921b60..0b97840 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationTest.java @@ -14,7 +14,8 @@ void constructor_CreatesAnOrganizationWhenInputIsValid() { true, "org.com", true, - "Org description" + "Org description", + null ); assertAll( @@ -35,7 +36,8 @@ void constructor_ThrowsWhenOrgNumberIsNegative() { true, "org.com", true, - "Org description" + "Org description", + null )); } @@ -47,7 +49,8 @@ void constructor_ThrowsWhenNameIsNull() { true, "org.com", true, - "Org description" + "Org description", + null )); } @@ -59,7 +62,8 @@ void constructor_ThrowsWhenNameIsBlank() { true, "org.com", true, - "Org description" + "Org description", + null )); } @@ -71,7 +75,8 @@ void constructor_ThrowsWhenWebsiteURLIsNull() { true, null, true, - "Org description" + "Org description", + null )); } @@ -83,7 +88,8 @@ void constructor_ThrowsWhenWebsiteURLIsBlank() { true, "", true, - "Org description" + "Org description", + null )); } @@ -95,6 +101,20 @@ void constructor_ThrowsWhenDescriptionIsNull() { true, "org.com", true, + null, + null + )); + } + + @Test + void constructor_AcceptsNullLogoUrl() { + assertDoesNotThrow(() -> new Organization( + 1, + "Org", + true, + "org.com", + true, + "description", null )); }