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/App.java b/src/main/java/edu/group5/app/App.java index c948b42..22b3baa 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -18,19 +18,33 @@ import java.util.List; +import java.util.logging.Logger; + /** * Main entry point for the Help-Me-Help charity donation application. * Handles database connection, data loading, and application setup. */ public class App extends Application { + DbWrapper dbWrapper; + UserRepository userRepository; + DonationRepository donationRepository; + + BorderPane root; + AppState appState; + NavigationController nav; + + private Logger logger; + @Override - public void start(Stage stage) { - DbWrapper dbWrapper = new DbWrapper(true); + public void init() { + this.logger = Logger.getLogger(App.class.getName()); + this.logger.info("Application starting"); + + this.dbWrapper = new DbWrapper(false); OrgApiWrapper orgApiWrapper = new OrgApiWrapper("https://app.innsamlingskontrollen.no/api/public/v1/all"); - if (!dbWrapper.connect()) { - System.err.println("Failed to connect to database"); - return; + while (!dbWrapper.connect()) { + this.logger.warning("Failed to connect to database"); } // Load data from database @@ -49,20 +63,23 @@ public void start(Stage stage) { } // Create repositories with fetched data - UserRepository userRepository = new UserRepository(userData); - DonationRepository donationRepository = new DonationRepository(donationData); + this.userRepository = new UserRepository(userData); + this.donationRepository = new DonationRepository(donationData); OrganizationRepository organizationRepository = new OrganizationRepository(organizationData); // Create services (backend wiring) - UserService userService = new UserService(userRepository); - DonationService donationService = new DonationService(donationRepository, organizationRepository); + UserService userService = new UserService(this.userRepository); + DonationService donationService = new DonationService(this.donationRepository, organizationRepository); OrganizationService organizationService = new OrganizationService(organizationRepository); - BorderPane root = new BorderPane(); - AppState appState = new AppState(); - NavigationController nav = new NavigationController(root, appState, userService, donationService, organizationService); + this.root = new BorderPane(); + this.appState = new AppState(); + this.nav = new NavigationController(root, appState, userService, donationService, organizationService); + } - nav.showLoginPage(); + @Override + public void start(Stage stage) { + this.nav.showLoginPage(); Scene scene = new Scene(root, 1280, 720); stage.getIcons().add(new Image(getClass().getResource("/header/images/hmh-logo.png").toExternalForm())); @@ -71,6 +88,16 @@ public void start(Stage stage) { stage.show(); } + @Override + public void stop() throws Exception { + super.stop(); + this.logger.info("Application stopping"); + this.dbWrapper.connect(); + this.dbWrapper.exportUsers(this.userRepository.export()); + this.dbWrapper.exportDonations(this.donationRepository.export()); + this.dbWrapper.disconnect(); + } + public static void main(String[] args) { launch(args); } diff --git a/src/main/java/edu/group5/app/control/OrganizationController.java b/src/main/java/edu/group5/app/control/OrganizationController.java index 9cbfc7a..499b7c9 100644 --- a/src/main/java/edu/group5/app/control/OrganizationController.java +++ b/src/main/java/edu/group5/app/control/OrganizationController.java @@ -5,6 +5,7 @@ import edu.group5.app.model.organization.OrganizationService; import java.util.Map; +import java.util.concurrent.CompletableFuture; public class OrganizationController { private final AppState appState; @@ -24,4 +25,8 @@ public Organization getOrgById(int orgId) { public Map getTrustedOrgs() { return service.getTrustedOrganizations(); } + + public CompletableFuture> getOrganizationsWithLogosAsync() { + return service.getTrustedOrganizationsWithLogosAsync(); + } } diff --git a/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java index babb929..7e3adc0 100644 --- a/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java +++ b/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java @@ -95,10 +95,13 @@ public List importUsers() { public int exportUsers(List data) { this.importUsers(); - + if (data == null) { throw new IllegalArgumentException("data can't be null"); } + if (data.isEmpty()) { + return 0; + } if (data.get(0).length != 6) { throw new IllegalArgumentException("data's arrays must have a length of 6"); } @@ -184,10 +187,13 @@ private List importDonations(int user_id, boolean all) { public int exportDonations(List data) { this.fetchAllDonations(); - + if (data == null) { throw new IllegalArgumentException("data can't be null"); } + if (data.isEmpty()) { + return 0; + } if (data.get(0).length != 6) { throw new IllegalArgumentException("data's arrays must have a length of 6"); } diff --git a/src/main/java/edu/group5/app/model/DBRepository.java b/src/main/java/edu/group5/app/model/DBRepository.java index f3363b7..3cd1fa9 100644 --- a/src/main/java/edu/group5/app/model/DBRepository.java +++ b/src/main/java/edu/group5/app/model/DBRepository.java @@ -1,18 +1,21 @@ package edu.group5.app.model; + import java.util.HashMap; import java.util.Map; import java.util.List; + /** * Abstract base class for repositories that store their data * in a database-related structure. * *

- * Extends {@link Repository} and specifies that the content - * is stored as a {@link Map}. + * Extends {@link Repository} and specifies that the content + * is stored as a {@link Map}. *

*/ public abstract class DBRepository extends Repository { protected final Map contentLock; + /** * Constructs a DBRepository with the given content. * @@ -23,19 +26,18 @@ protected DBRepository(Map content) { this.contentLock = new HashMap<>(); } - protected void updateContentLock() { - synchronized (contentLock) { - contentLock.clear(); - contentLock.putAll(this.content); - } - } + protected abstract void updateContentLock(); public abstract boolean addContent(V value); /** - * Exports the repository content as a list of Object arrays, where each array represents a row of data. - * This method is intended for converting the repository content into a format suitable for database storage or export. - * @return a List of Object arrays representing the repository content for database export + * Exports the repository content as a list of Object arrays, where each array + * represents a row of data. + * This method is intended for converting the repository content into a format + * suitable for database storage or export. + * + * @return a List of Object arrays representing the repository content for + * database export */ public abstract List export(); } diff --git a/src/main/java/edu/group5/app/model/donation/DonationRepository.java b/src/main/java/edu/group5/app/model/donation/DonationRepository.java index 6181460..a55ea74 100644 --- a/src/main/java/edu/group5/app/model/donation/DonationRepository.java +++ b/src/main/java/edu/group5/app/model/donation/DonationRepository.java @@ -15,7 +15,7 @@ * Repository class for Donation. * *

- * Extends {@link DBRepository} and manages Donation entities. + * Extends {@link DBRepository} and manages Donation entities. *

*/ public class DonationRepository extends DBRepository { @@ -23,10 +23,12 @@ public class DonationRepository extends DBRepository { /** * Constructs DonationRepository from a list of Object[] rows from the database. - * @param rows List of Object[] representing donations from the DB. - * Each row must have 6 elements: - * [donationId, userId, organizationId, amount, date, paymentMethod] - * @throws IllegalArgumentException if the input list is null or any row is invalid + * + * @param rows List of Object[] representing donations from the DB. + * Each row must have 6 elements: + * [donationId, userId, organizationId, amount, date, paymentMethod] + * @throws IllegalArgumentException if the input list is null or any row is + * invalid */ public DonationRepository(List rows) { super(new HashMap<>()); @@ -35,12 +37,12 @@ public DonationRepository(List rows) { } this.content = new HashMap<>(); for (Object[] row : rows) { - if (row == null || row.length != 6 ) { + if (row == null || row.length != 6) { throw new IllegalArgumentException("Each row must contain exactly 6 elements"); } int donationId = (int) row[0]; int customerId = (int) row[1]; - int organizationId = (int) row[2]; + int organizationId = (int) row[2]; BigDecimal amount = (BigDecimal) row[3]; Timestamp date = (Timestamp) row[4]; String paymentMethod = (String) row[5]; @@ -48,23 +50,39 @@ public DonationRepository(List rows) { Donation donation = new Donation(donationId, customerId, organizationId, amount, date, paymentMethod); this.content.put(donationId, donation); } - super.updateContentLock(); + this.updateContentLock(); + } + + @Override + protected void updateContentLock() { + synchronized (contentLock) { + this.contentLock.clear(); + this.contentLock.putAll(this.content); + } } @Override public List export() { - return content.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .map(entry -> { Donation donation = entry.getValue(); - return new Object[] { - donation.donationId(), donation.userId(), - donation.organizationId(), donation.amount(), - donation.date(), donation.paymentMethod()};}) - .toList(); + Map output = new HashMap<>(this.content); + for (int i : super.contentLock.keySet()) { + output.remove(i); + } + this.updateContentLock(); + return output.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(entry -> { + Donation donation = entry.getValue(); + return new Object[] { + donation.donationId(), donation.userId(), + donation.organizationId(), donation.amount(), + donation.date(), donation.paymentMethod() }; + }) + .toList(); } /** * Retrieves a donation by its ID. + * * @param donationId the ID of the donation to retrieve * @return the Donation object with the specified ID, or null if not found * @throws IllegalArgumentException if the donationId is not positive @@ -77,10 +95,12 @@ public Donation getDonationById(int donationId) { } /** - * Generates the next donation ID based on the current maximum ID in the repository. + * Generates the next donation ID based on the current maximum ID in the + * repository. + * * @return the next donation ID to be used for a new donation */ - public int getNextDonationId() { + public int getNextDonationId() { return content.keySet().stream().max(Integer::compareTo).orElse(0) + 1; } /* TODO change this when data database is introduced */ @@ -91,22 +111,22 @@ public Map getAllDonations() { /** * Adds a new donation to the repository *

- * The donation is stored using its {@code donationId} as the key. - * If a donation with the same ID already exists, the donation - * will not be added. + * The donation is stored using its {@code donationId} as the key. + * If a donation with the same ID already exists, the donation + * will not be added. *

* * @param donation the donation to add * @return {@code true} if the donation was successfully added, and - * {@code false} if a donation with the same ID already exists + * {@code false} if a donation with the same ID already exists */ @Override public boolean addContent(Donation donation) { if (donation == null) { throw new IllegalArgumentException("Donation cannot be null"); } - if (content.containsKey(donation.donationId())){ - return false; + if (content.containsKey(donation.donationId())) { + return false; } this.content.put(donation.donationId(), donation); return true; @@ -116,12 +136,12 @@ public boolean addContent(Donation donation) { * Returns all donations sorted by date (ascending). * *

- * The returned map preserves the sorted order. + * The returned map preserves the sorted order. *

* * @return a new {@link HashMap} containing the donations sorted by date */ - public HashMap sortByDate(){ + public HashMap sortByDate() { return content.entrySet().stream() .sorted(Map.Entry.comparingByValue( Comparator.comparing(Donation::date))) @@ -136,7 +156,7 @@ public HashMap sortByDate(){ * Returns all donations sorted by amount (ascending). * *

- * The returned map preserves the sorted order from lowest to highest amount. + * The returned map preserves the sorted order from lowest to highest amount. *

* * @return a new {@link HashMap} containing the donations sorted by amount. @@ -149,12 +169,12 @@ public HashMap sortByAmount() { Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, - LinkedHashMap::new - )); + LinkedHashMap::new)); } /** * Returns all donations associated with a specific organization. + * * @param orgNumber the organization ID to filter by * @return a map containing all donations that belong to the given organization * @throws IllegalArgumentException if the orgNumber is not positive @@ -165,17 +185,17 @@ public HashMap filterByOrganization(int orgNumber) { } return content.entrySet() .stream() - .filter(entry -> entry.getValue().organizationId() == orgNumber) + .filter(entry -> entry.getValue().organizationId() == orgNumber) .collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, - LinkedHashMap::new - )); + LinkedHashMap::new)); } /** * Returns all donations made by a specific user. + * * @param userId the user ID to filter by * @return a map containing all donations that belong to the given user * @throws IllegalArgumentException if the userId is not positive @@ -190,7 +210,6 @@ public HashMap filterByUser(int userId) { Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, - LinkedHashMap::new - )); + LinkedHashMap::new)); } } 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/model/user/UserRepository.java b/src/main/java/edu/group5/app/model/user/UserRepository.java index 692126f..193f433 100644 --- a/src/main/java/edu/group5/app/model/user/UserRepository.java +++ b/src/main/java/edu/group5/app/model/user/UserRepository.java @@ -6,11 +6,13 @@ import edu.group5.app.model.DBRepository; -public class UserRepository extends DBRepository{ +public class UserRepository extends DBRepository { public final static String ROLE_CUSTOMER = "Customer"; + /** * Constructs UserRepository using Hashmap, * and extends the content from DBRepository. + * * @param content the underlying map used to store users, * where the key represents the user ID */ @@ -20,7 +22,7 @@ public UserRepository(List rows) { throw new IllegalArgumentException("The list of rows cannot be null"); } for (Object[] row : rows) { - if (row == null || row.length != 6 ) { + if (row == null || row.length != 6) { throw new IllegalArgumentException("Each row must contain exactly 6 elements"); } int userId = (int) row[0]; @@ -38,28 +40,46 @@ public UserRepository(List rows) { } this.content.put(userId, user); } - super.updateContentLock(); + this.updateContentLock(); } + @Override + protected void updateContentLock() { + synchronized (contentLock) { + this.contentLock.clear(); + this.contentLock.putAll(this.content); + } + } @Override public List export() { - return content.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .map(entry -> { User user = entry.getValue(); - return new Object[]{user.getUserId(), user.getRole(), - user.getFirstName(), user.getLastName(), - user.getEmail(), user.getPasswordHash()};}) + Map output = new HashMap<>(this.content); + for (int i : contentLock.keySet()) { + output.remove(i); + } + this.updateContentLock(); + return output.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(entry -> { + User user = entry.getValue(); + return new Object[] { user.getUserId(), user.getRole(), + user.getFirstName(), user.getLastName(), + user.getEmail(), user.getPasswordHash() }; + }) .toList(); + } public HashMap getUsers() { return new HashMap<>(content); } + /** * Retrieves a user by their unique identifier. + * * @param userId the unique identifier of the user to retrieve - * @return the user with the specified ID, or {@code null} if no such user exists + * @return the user with the specified ID, or {@code null} if no such user + * exists * @throws IllegalArgumentException if the userId is not positive */ public User getUserById(int userId) { @@ -72,12 +92,13 @@ public User getUserById(int userId) { /** * Generates the next user ID based on repository size. * Uses size+1 and then moves forward if that ID is already taken. + * * @return the next available user ID * @throws IllegalStateException if no available user ID can be found */ - public int getNextUserId() { + public int getNextUserId() { if (content.isEmpty()) { - return 1; + return 1; } int maxKey = content.keySet().stream().max(Integer::compareTo).orElseThrow( () -> new IllegalStateException("No keys found")); @@ -88,21 +109,21 @@ public int getNextUserId() { /** * Adds a new user to the repository *

    - * The user is stored using its {@code userId} as the key. - * If a user with the same ID already exists, the user - * will not be added. + * The user is stored using its {@code userId} as the key. + * If a user with the same ID already exists, the user + * will not be added. *

    * * @param user the user to add * @return {@code true} if the user was successfully added, and - * {@code false} if a user with the same ID already exists + * {@code false} if a user with the same ID already exists */ @Override public boolean addContent(User user) { if (user == null) { throw new IllegalArgumentException("User cannot be null"); } - if (content.containsKey(user.getUserId())){ + if (content.containsKey(user.getUserId())) { return false; } this.content.put(user.getUserId(), user); @@ -111,8 +132,10 @@ public boolean addContent(User user) { /** * Finds a user by their email address. + * * @param email the email address of the user to find - * @return the user with the specified email address, or {@code null} if no such user exists + * @return the user with the specified email address, or {@code null} if no such + * user exists */ public User findUserByEmail(String email) { if (email == null || email.trim().isEmpty()) { @@ -122,5 +145,5 @@ public User findUserByEmail(String email) { .filter(user -> user.getEmail().equals(email)) .findFirst() .orElse(null); - } + } } diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index 4784bcd..1cd6e83 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -1,9 +1,10 @@ package edu.group5.app.view.causespage; +import edu.group5.app.model.organization.Organization; import edu.group5.app.control.NavigationController; import edu.group5.app.control.OrganizationController; import edu.group5.app.model.AppState; -import edu.group5.app.model.organization.Organization; +import javafx.application.Platform; import javafx.scene.control.ScrollPane; import javafx.scene.control.TextField; import javafx.scene.layout.*; @@ -70,8 +71,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 = orgController.getTrustedOrgs(); + + //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) + orgController.getOrganizationsWithLogosAsync() + .thenAccept(orgs -> { + this.allOrganizations = orgs; + + // Update UI when data is ready + Platform.runLater(() -> updateOrganizationGrid("")); + }); + return grid; } Map organizations = new HashMap<>(); @@ -84,9 +103,14 @@ private GridPane createOrganizationSection(String searchTerm) { int column = 0; int row = 0; + for (Organization org : organizations.values()) { - String defaultImg = "/browsepage/images/children_of_shambala.png"; - OrganizationCard card = new OrganizationCard(appState, nav, org, defaultImg); + //Adds default text if organization does not have any + String img = (org.logoUrl() != null && !org.logoUrl().isBlank()) + ? org.logoUrl() + : null; + + OrganizationCard card = new OrganizationCard(appState, nav, org, img); grid.add(card, column, row); diff --git a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java index 389b934..31b25ce 100644 --- a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java +++ b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java @@ -41,20 +41,32 @@ public OrganizationCard(AppState appstate, NavigationController nav, Organizatio 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/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 93cace6..da8d1df 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 = appState.getCurrentOrganization(); - String imagePath = "/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/control/wrapper/DbWrapperDonationsTest.java b/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java index f31d57c..8c401c4 100644 --- a/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java +++ b/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java @@ -196,4 +196,12 @@ public void addingDonationListWithNullInRowThrowsExpectedException() { assertTrue(this.db.disconnect()); assertEquals("One or more rows in data contains null values", exception.getMessage()); } + + @Test + public void dataIsEmptyAfterExportingAndImportingEmptyList() { + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); + assertEquals(0, this.db.exportDonations(new ArrayList())); + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); + assertTrue(this.db.disconnect()); + } } diff --git a/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java b/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java index bdf0110..ce81b2a 100644 --- a/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java @@ -263,4 +263,13 @@ void filterByUserIdThrowsIfNegative() { () -> repo.filterByUser(0)); assertEquals("User ID must be positive", ex.getMessage()); } + + @Test + void exportExportsOnlyNewInformation() { + repo.addContent(donation1); + repo.addContent(donation2); + assertEquals(2, repo.export().size()); + repo.addContent(donation3); + assertEquals(1, repo.export().size()); + } } \ No newline at end of file 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 )); } diff --git a/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java b/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java index 0700828..eaf2c2c 100644 --- a/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java @@ -12,6 +12,7 @@ public class UserRepositoryTest { private UserRepository repo; private List rows; +private User additionalUser; @BeforeEach void setUp() { @@ -19,6 +20,7 @@ void setUp() { rows.add(new Object[]{1, "Customer", "John", "Cena", "john@example.com", "hashedpass"}); rows.add(new Object[]{2, "Customer", "Jane", "Doe", "jane@example.com", "hashedpass"}); repo = new UserRepository(rows); + this.additionalUser = new Customer(3, "John", "Doe", "john@example.com", "hashedpass"); } @Test @@ -138,11 +140,9 @@ void getNextUserIdReturns1IfEmpty() { } @Test - void exportContainsAllUsers() { - List exported = repo.export(); - assertEquals(2, exported.size()); - assertEquals(1, exported.get(0)[0]); - assertEquals(2, exported.get(1)[0]); - assertEquals("Customer", exported.get(0)[1]); + void exportExportsOnlyNewInformation() { + assertEquals(0, repo.export().size()); + repo.addContent(this.additionalUser); + assertEquals(1, repo.export().size()); } } \ No newline at end of file diff --git a/src/test/java/edu/group5/app/model/user/UserServiceTest.java b/src/test/java/edu/group5/app/model/user/UserServiceTest.java index 8f56957..acec10b 100644 --- a/src/test/java/edu/group5/app/model/user/UserServiceTest.java +++ b/src/test/java/edu/group5/app/model/user/UserServiceTest.java @@ -149,4 +149,20 @@ void loginInvalidEmail() { assertNull(result2); assertNull(result3); } + + @Test + void getUserByEmailValid() { + User result = service.getUserByEmail("jane.doe@example.com"); + assertEquals("Jane", result.getFirstName()); + } + + @Test + void getUserByEmailNotFound() { + User result = service.getUserByEmail(""); + User result2 = service.getUserByEmail(null); + User result3 = service.getUserByEmail(" "); + assertNull(result); + assertNull(result2); + assertNull(result3); + } } \ No newline at end of file